Category: Coding

Kafbat – OAUTH With RBAC

I encountered a challenge with a Kafka management tool — it supports SSO, and I was able to get an OAUTH connection set up to control what users could see when logging in through the UI, but the API component didn’t extract information from the bearer token and there was nothing in the rbac mapping to allow the bearer-token client ID to access anything.

Updates to allow the /api components to be authenticated by simple bearer tokens and a client ID mapped into a role are at https://github.com/ljr55555/kafka-ui

AuthorizationController.java was updated to properly support non-browser, machine-principal auth.

Added/fixed:

  • avoids null failure when authentication.getName() is missing
  • resolves principal name from alternate attributes such as:
    • client_id
    • sub
    • username
  • updated displayed permissions logic so /api/authorization uses the same effective RBAC matching idea as the backend

Result

/api/authorization now works for bearer-token API callers and shows:

  • username = client ID
  • populated permissions list

AccessControlService.java

Added support for API bearer-token principals

Previously, getUser() only worked when the authenticated principal was a RbacUser, which covered the browser/user flow.

Now it can also derive an AuthenticatedUser from opaque-token authenticated principals by extracting:

  • principal name
  • group-like values from attributes/authorities if present

Updated role matching logic

Previously, role matching was only role name matches one of user.groups(). Now it also supports role name matches user.principal(). That enables RBAC binding directly to the API client ID.

Result

RBAC now works for:

  • normal browser users via groups
  • API bearer-token callers via client principal name

DynamicConfigMapper.java

Fixed a mapper bug.

Before

The method mapping resource server config created a populated OAuth2ResourceServerProperties result object but always returned null.

After

It now returns result.

Result

Dynamic/config mapping for resource-server settings no longer silently discards the mapped object.

Build/package note

To preserve the browser UI, the jar needs to be built with frontend included — which you know if you read the doc … or you take my route, start it all up, test the API successfully, and then get baffled that the user UI throws


        bash./gradlew clean assemble -Pinclude-frontend=true

application.yml

server:
  port: 8443
  ssl:
    enabled: true
    key-store: file:/etc/kafkaui/certs/kafbat.rushworth.us.p12
    key-store-password: ${KEYSTORE_PASSWORD}
    key-store-type: PKCS12
    key-alias: kafbat

auth:
  type: OAUTH2
  oauth2:
    client:
      pingfed:
        client-id: ${OAUTH_CLIENT_ID}
        client-secret: ${OAUTH_CLIENT_SECRET}
        scope:
          - openid
          - profile
          - email
        client-name: oauthclient
        provider: oauthclient
        redirect-uri: https://kafbat.rushworth.us:8443/login/oauth2/code/oauthclient
        authorization-grant-type: authorization_code
        issuer-uri: https://login.example.com
        jwk-set-uri: https://login.example.com/pf/JWKS
        authorization-uri: https://login.example.com/as/authorization.oauth2
        token-uri: https://login.example.com/as/token.oauth2
        user-info-uri: https://login.example.com/idp/userinfo.openid
        user-name-attribute: username
        custom-params:
          type: oauth
          roles-field: memberOf

    resource-server:
      opaque-token:
        client-id: ${OAUTH_CLIENT_ID}
        client-secret: ${OAUTH_CLIENT_SECRET}
        introspection-uri: https://login.example.com/as/introspect.oauth2

kafka:
  clusters:
    - name: test
      bootstrapServers: ${KAFKA_BOOTSTRAP_SERVERS} 

roles.yml

rbac:
  roles:
    - name: "admins"
      clusters:
        - test
      subjects:
        - provider: oauth
          type: role
          value: "CN=KafbatAdmins,OU=SecurityGroups,DC=example,DC=com"
      permissions:
        - resource: applicationconfig
          actions: all

        - resource: clusterconfig
          actions: all

        - resource: topic
          value: ".*"
          actions: all

        - resource: consumer
          value: ".*"
          actions: all

        - resource: schema
          value: ".*"
          actions: all

        - resource: connect
          value: ".*"
          actions: all

        - resource: ksql
          actions: all

        - resource: acl
          actions: [ view ]

    - name: "${OAUTH_CLIENT_ID}"
      clusters:
        - test
      subjects:
        - provider: oauth
          type: user
          value: "${OAUTH_CLIENT_ID}"
      permissions:
        - resource: applicationconfig
          actions: all

        - resource: clusterconfig
          actions: all

        - resource: topic
          value: ".*"
          actions: all

        - resource: consumer
          value: ".*"
          actions: all

        - resource: schema
          value: ".*"
 
        - resource: connect
          value: ".*"
          actions: all

        - resource: ksql
          actions: all

        - resource: acl
          actions: [ view ]
 

docker-compose.yml

services:
  redpanda:
    image: redpandadata/redpanda:v25.1.2
    container_name: redpanda
    command:
      - redpanda
      - start
      - --overprovisioned
      - --smp=1
      - --memory=1G
      - --reserve-memory=0M
      - --node-id=0
      - --check=false
      - --kafka-addr=PLAINTEXT://0.0.0.0:9092
      - --advertise-kafka-addr=PLAINTEXT://redpanda:9092
    ports:
      - "9092:9092"

  kafbat-ui:
    image: ghcr.io/kafbat/kafka-ui:latest
    container_name: kafbat-ui
    restart: unless-stopped
    depends_on:
      - redpanda
    ports:
      - "8443:8443"
    volumes:
      - ./config/application.yml:/etc/kafkaui/application.yml:ro
      - ./config/roles.yml:/etc/kafkaui/roles.yml:ro
      - ./certs:/etc/kafkaui/certs:ro
    environment:
      SPRING_CONFIG_LOCATION: file:/etc/kafkaui/application.yml
      SPRING_CONFIG_ADDITIONAL_LOCATION: file:/etc/kafkaui/roles.yml
      KEYSTORE_PASSWORD: REDACTED
      OAUTH_CLIENT_ID: REDACTED
      OAUTH_CLIENT_SECRET: REDACTED
      KAFKA_BOOTSTRAP_SERVERS: redpanda:9092

Docker for Java Builds

Instead of flipping back and forth between java versions for various builds, you can just use a docker container for the proper Java version to run the build.

[lisa@docker kafka-ui]# docker run --rm -it   --user $(id -u):$(id -g)   -v "$PWD":/workspace   -w /workspace   eclipse-temurin:25   bash -lc './gradlew clean build'
Downloading https://services.gradle.org/distributions/gradle-9.2.0-bin.zip
............10%.............20%.............30%.............40%.............50%.............60%.............70%.............80%.............90%.............100%

Welcome to Gradle 9.2.0!

Here are the highlights of this release:
 - Windows ARM support
 - Improved publishing APIs
 - Better guidance for dependency verification failures

For more details see https://docs.gradle.org/9.2.0/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)
<=============> 100% CONFIGURING [1m 46s]
> Resolve dependencies of :api:detachedConfiguration273

Blender API: List All Items in a Collection

Instead of iterating through all objects, you can iterate through the items in a specific collection:

import bpy

# Name of the collection to inspect
collection_name = "TestCollection"

collection = bpy.data.collections.get(collection_name)

if collection is None:
    print(f"Collection '{collection_name}' not found.")
else:
    print(f"Objects in collection '{collection_name}':")
    for obj in collection.objects:
        print(f"- {obj.name}")

Printing to the console:

For this sample workspace that contains a torus and sphere with the default names

Blender API: Finding The Orange Dot

A quick script to get each object and the location of the “orange dot” … the origin of the object

# Get location of orange dot for each object in Blender
import bpy

scene = bpy.context.scene
us = scene.unit_settings

unit_system = getattr(us, "system", "NONE")  # 'NONE', 'METRIC', 'IMPERIAL'

meters_per_bu = us.scale_length if unit_system != 'NONE' else 1.0
mm_per_bu = meters_per_bu * 1000.0

for obj in bpy.data.objects:
    if obj.type != 'MESH':
        continue

    origin_world = obj.matrix_world.translation          # in BU
    origin_world_mm = origin_world * mm_per_bu           # in mm

    print(f"Object: {obj.name}")
    print(f"  origin_world (BU): {origin_world.x:.6f}, {origin_world.y:.6f}, {origin_world.z:.6f}")
    print(f"  origin_world (mm): {origin_world_mm.x:.3f}, {origin_world_mm.y:.3f}, {origin_world_mm.z:.3f}")
    print("-" * 30)

Blender API: Bending a 2D Rectangle

Another attempt to create a t-post bracket using a script. This creates a 2D rectangle, bends it, and then solidifies it into a 3d object.

import bpy
import bmesh
import math
from mathutils import Vector, Matrix

# -----------------------------
# Reset / clear scene
# -----------------------------
for obj in list(bpy.data.objects):
    bpy.data.objects.remove(obj, do_unlink=True)

# -----------------------------
# Scene units: mm (1 BU = 1 mm)
# -----------------------------
scene = bpy.context.scene
scene.unit_settings.system = 'METRIC'
scene.unit_settings.scale_length = 0.001

INCH_TO_MM = 25.4
def inch(x):  # returns mm (Blender units)
    return x * INCH_TO_MM

# -----------------------------
# Parameters
# -----------------------------
size_x_in = 3.0
size_y_in = 7.0
thickness_in = 0.25  # SOLIDIFY thickness

fold1_offset_in = 0.5   # from MIN-Y end
fold2_offset_in = 2.0   # from MIN-Y end

fold1_rad = math.radians(-80.0)
fold2_rad = math.radians(80.0)

subdivide_cuts = 60
EPS_Y = 1e-5  # mm tolerance for "on the fold line"

# -----------------------------
# Create flat sheet (plane)
# -----------------------------
bpy.ops.mesh.primitive_plane_add(size=1.0, location=(0.0, 0.0, 0.0))
obj = bpy.context.active_object
obj.name = "Bracket"
obj.dimensions = (inch(size_x_in), inch(size_y_in), 0.0)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)

# Subdivide for clean fold lines
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.subdivide(number_cuts=subdivide_cuts)
bpy.ops.object.mode_set(mode='OBJECT')

# Compute fold Y positions
half_y = inch(size_y_in) / 2.0
min_y = -half_y
y_fold1 = min_y + inch(fold1_offset_in)
y_fold2 = min_y + inch(fold2_offset_in)

# Add both fold lines
bm = bmesh.new()
bm.from_mesh(obj.data)

for y_fold in (y_fold1, y_fold2):
    geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
    bmesh.ops.bisect_plane(
        bm,
        geom=geom,
        plane_co=Vector((0.0, y_fold, 0.0)),
        plane_no=Vector((0.0, 1.0, 0.0)),
        clear_inner=False,
        clear_outer=False
    )

bm.normal_update()
bm.to_mesh(obj.data)
bm.free()

# -----------------------------
# Re-open bmesh, store ORIGINAL Y per vertex
# -----------------------------
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.verts.ensure_lookup_table()

orig_y_layer = bm.verts.layers.float.new("orig_y")
for v in bm.verts:
    v[orig_y_layer] = v.co.y

# ============================================================
# FOLD 1
# ============================================================
hinge_verts_1 = [v for v in bm.verts if abs(v[orig_y_layer] - y_fold1) < EPS_Y]
if not hinge_verts_1:
    raise RuntimeError("No hinge vertices found for fold 1. Increase subdivide_cuts or EPS_Y.")

hinge_point_1 = Vector((0.0, 0.0, 0.0))
for v in hinge_verts_1:
    hinge_point_1 += v.co
hinge_point_1 /= len(hinge_verts_1)

verts_to_rotate_1 = [v for v in bm.verts if v[orig_y_layer] > (y_fold1 + EPS_Y)]
rot1 = Matrix.Rotation(fold1_rad, 4, 'X')
bmesh.ops.rotate(bm, verts=verts_to_rotate_1, cent=hinge_point_1, matrix=rot1)

# ============================================================
# FOLD 2
# ============================================================
hinge_verts_2 = [v for v in bm.verts if abs(v[orig_y_layer] - y_fold2) < EPS_Y]
if not hinge_verts_2:
    raise RuntimeError("No hinge vertices found for fold 2. Increase subdivide_cuts or EPS_Y.")

hinge_point_2 = Vector((0.0, 0.0, 0.0))
for v in hinge_verts_2:
    hinge_point_2 += v.co
hinge_point_2 /= len(hinge_verts_2)

verts_to_rotate_2 = [v for v in bm.verts if v[orig_y_layer] > (y_fold2 + EPS_Y)]
rot2 = Matrix.Rotation(fold2_rad, 4, 'X')
bmesh.ops.rotate(bm, verts=verts_to_rotate_2, cent=hinge_point_2, matrix=rot2)

# Write back mesh
bm.normal_update()
bm.to_mesh(obj.data)
bm.free()

# -----------------------------
# Solidify AFTER folding
# -----------------------------
solid = obj.modifiers.new(name="Solidify_0p5in", type='SOLIDIFY')
solid.thickness = inch(thickness_in)  # 0.5"
solid.offset = 0.0                    # centered thickness (equal on both sides)
solid.use_even_offset = True
solid.use_rim = True

# Optional: keep object active
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj

Blender API: Playing with Cylinders

This script was mostly made to play around with rotation on cylinders.

import bpy
import math

# Delete all existing objects
for obj in list(bpy.data.objects):
    bpy.data.objects.remove(obj, do_unlink=True)

for i in range(4):
    bpy.ops.mesh.primitive_cylinder_add(
        radius=0.5,
        depth=10.0,
        location=(0, 0.0, 0.0),
        rotation=((i * 5.5), 0.0, 0.0)
    )

    cyl = bpy.context.active_object
    cyl.name = f"DemoCylinderX{i}"

for i in range(4):
    bpy.ops.mesh.primitive_cylinder_add(
        radius=0.5,
        depth=10.0,
        location=(0, 0.0, 0.0),
        rotation=(0.0, (i * 5.5), 0.0)
    )

    cyl = bpy.context.active_object
    cyl.name = f"DemoCylinderY{i}"

# cyl.rotation_euler = (15.0,13.0,12.0)

# Or single-axis rotation
# Rotate 45 degrees about X axis
#cyl.rotation_euler[0] = math.radians(45.0)


Blender Scripting Lesson of the Week: Beveling

We were playing around with bevels this week – it’s pretty straight forward, the API lets you set the parameters you set through the GUI in a bevel modifier.

import bpy

# Clear all existing objects
for obj in list(bpy.data.objects):
    bpy.data.objects.remove(obj, do_unlink=True)

# Set Units
scene = bpy.context.scene
scene.unit_settings.system = 'METRIC'
scene.unit_settings.scale_length = 0.001  # 1 BU = 1 mm

# Create rectangular cube
bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0))
block = bpy.context.active_object
block.name = "Block"

# cube default size is 2x2x2, so set absolute dimensions
block.dimensions = (2.0, 20.0, 0.25)
bpy.context.view_layer.objects.active = block
block.select_set(True)

# Apply scale so booleans/bevel behave predictably
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)

# Create cylinder cutter
hole_diameter = 1.0
hole_radius = hole_diameter / 2.0

# Make it longer than the block thickness so it fully cuts through
cutter_depth = 5.0

bpy.ops.mesh.primitive_cylinder_add(
    vertices=64,
    radius=hole_radius,
    depth=cutter_depth,
    location=(0.0, 0.0, 0.0),   # center of the block
    rotation=(0.0, 0.0, 0.0)
)
cutter = bpy.context.active_object
cutter.name = "HoleCutter"

bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)

# Boolean: cut hole
bpy.context.view_layer.objects.active = block
bool_mod = block.modifiers.new(name="Hole", type='BOOLEAN')
bool_mod.operation = 'DIFFERENCE'
bool_mod.solver = 'EXACT'
bool_mod.object = cutter

# Apply boolean
bpy.ops.object.modifier_apply(modifier=bool_mod.name)

# Hide cutter in viewport + renders
cutter.hide_set(True)
cutter.hide_render = True

# Bevel the block
bevel_width = 0.08 
bevel_segments = 5

bevel_mod = block.modifiers.new(name="Bevel", type='BEVEL')
bevel_mod.width = bevel_width
bevel_mod.segments = bevel_segments
bevel_mod.limit_method = 'ANGLE'
bevel_mod.angle_limit = 0.523599  # 30 degrees in radians

# Apply bevel
bpy.ops.object.modifier_apply(modifier=bevel_mod.name)

Blender Scripting – T-Post Sign Holder

We spent a lot of the day trying to modify 3D models that we found online to work as a sign holder. Something like the bent metal plates you can buy at the tractor store. Since these are simple polygons, I thought it might be easier to script the build (plus making changes to the dimensions would just require tweaking variables).

Voila – hopefully it’s a T-post sign holder! It at least looks like one.

import bpy
import bmesh
import math
from mathutils import Vector

# Clear all existing objects
for obj in list(bpy.data.objects):
    bpy.data.objects.remove(obj, do_unlink=True)

# -----------------------------
# Scene units (mm)
# -----------------------------
scene = bpy.context.scene
scene.unit_settings.system = 'METRIC'
scene.unit_settings.scale_length = 0.001  # 1 Blender unit = 1 mm

INCH = 25.4
def inch(x): return x * INCH

# -----------------------------
# PARAMETERS (mm)
# -----------------------------
bracket_thickness = inch(0.25)   # sheet thickness
bracket_width = inch(3)   # bracket width (across the post)

# Leg lengths (side profile)
bracket_top_length = inch(1)        # bracket segment 1 length
bracket_middle_length = inch(2)     # bracket segment 2 length
bracket_bottom_length = inch(4.5)   # bracket segment 3 length

# Bend included angles
bend1_angle_included = 105.0   # top flange
bend2_angle_included = 255.0   # web -> long leg

# If the long leg goes the wrong direction, flip this
flip_second_bend = True

# -----------------------------
# Punch hole
# -----------------------------
do_punch = True

# T-post size references
tpost_horizontal_hole_height = inch(0.25)
tpost_horizontal_hole_width = inch(1.5)
tpost_vertical_hole_height = inch(2)
tpost_vertical_hole_width = inch(0.25)
punch_clearance = 1.0 # clearance added around each rectangle (mm)

# Position of t-post before rotation (Z from p0 end, and X across width)
punch_center_z = inch(1)
punch_center_x = bracket_width / 2

# Vertical placement on top flange (Y=0 plane)
punch_center_y = -inch(0.5)

# -----------------------------
# Optional bevel to make edges25ook more formed
# -----------------------------
do_bevel = True

bevel_width = inch(0.05)
bevel_segments = 25

# -----------------------------
# Cleanup
# -----------------------------
#for n in ["BracketShape", "PunchBar", "PunchStem", "HoleRight1", "HoleRight2", "HoleRight3", "HoleRight4", "HoleLeft1", "HoleRLeft2", "HoleLeft3", "HoleLeft4"]:
#    o = bpy.data.objects.get(n)
#    if o:
#        bpy.data.objects.remove(o, do_unlink=True)

# -----------------------------
# Helpers (YZ plane directions)
# Define 0° as +Z. +90° is +Y. -90° is -Y.
# -----------------------------
def unit_from_angle(deg_from_posZ):
    a = math.radians(deg_from_posZ)
    return Vector((0.0, math.sin(a), math.cos(a)))

def boolean_diff(target, cutter):
    mod = target.modifiers.new(name=f"BOOL_{cutter.name}", type="BOOLEAN")
    mod.operation = 'DIFFERENCE'
    mod.solver = 'EXACT'
    mod.object = cutter
    bpy.context.view_layer.objects.active = target
    bpy.ops.object.modifier_apply(modifier=mod.name)
    cutter.hide_set(True)

def add_cube(name, size_xyz, location_xyz, rotation_xyz):
    bpy.ops.mesh.primitive_cube_add(size=1, location=location_xyz, rotation=rotation_xyz)
    obj = bpy.context.active_object
    obj.name = name
    obj.scale = (size_xyz[0], size_xyz[1], size_xyz[2])
    bpy.ops.object.transform_apply()
    return obj

def add_cylinder(name, radius, length, location_xyz, rotation_xyz):
    bpy.ops.mesh.primitive_cylinder_add(radius=radius, depth=length, location=location_xyz, rotation=rotation_xyz)
    obj = bpy.context.active_object
    obj.name = name
    bpy.ops.object.transform_apply()
    return obj

# Convert included bend angles to turn angles
angle_top = 180.0 - bend1_angle_included
angle_bottom = 180.0 - bend2_angle_included

# Start along +Z (top flange)
theta0 = 0.0
d0 = unit_from_angle(theta0)

# After bend1, go "down" (toward -Y) by turning negative
theta1 = theta0 - angle_top
d1 = unit_from_angle(theta1)

# After bend2, go toward +Z again (or flip if needed)
theta2 = theta1 + (angle_bottom if not flip_second_bend else - angle_bottom)
d2 = unit_from_angle(theta2)

# Profile points (center surface)
p0 = Vector((0.0, 0.0, 0.0))      # free end of top flange
p1 = p0 + d0 * bracket_top_length       # bend1 line
p2 = p1 + d1 * bracket_middle_length    # bend2 line
p3 = p2 + d2 * bracket_bottom_length    # end of long leg

# -----------------------------
# Build a single connected sheet surface:
# Create two polylines separated in X, then make quads between them.
# -----------------------------
mesh = bpy.data.meshes.new("BracketShapeMesh")
bracket = bpy.data.objects.new("BracketShape", mesh)
bpy.context.collection.objects.link(bracket)
bpy.context.view_layer.objects.active = bracket
bracket.select_set(True)

bm = bmesh.new()

x0, x1 = 0.0, bracket_width

# Left side (x0)
v0a = bm.verts.new((x0, p0.y, p0.z))
v1a = bm.verts.new((x0, p1.y, p1.z))
v2a = bm.verts.new((x0, p2.y, p2.z))
v3a = bm.verts.new((x0, p3.y, p3.z))

# Right side (x1)
v0b = bm.verts.new((x1, p0.y, p0.z))
v1b = bm.verts.new((x1, p1.y, p1.z))
v2b = bm.verts.new((x1, p2.y, p2.z))
v3b = bm.verts.new((x1, p3.y, p3.z))

# Faces (one per segment)
bm.faces.new((v0a, v0b, v1b, v1a))  # top flange
bm.faces.new((v1a, v1b, v2b, v2a))  # web
bm.faces.new((v2a, v2b, v3b, v3a))  # long leg

bm.normal_update()
bm.to_mesh(mesh)
bm.free()

# -----------------------------
# Solidify to thickness (sheet metal look)
# -----------------------------
solid = bracket.modifiers.new("Solidify", type="SOLIDIFY")
solid.thickness = bracket_thickness
solid.offset = 0.0
bpy.ops.object.modifier_apply(modifier=solid.name)

# -----------------------------
# Punch the lowercase "t" on the top flange
# (Top flange is flat at Y=0; punch straight through Y)
# -----------------------------
if do_punch:
    tpost_length_y = bracket_thickness * 5  # ensure it fully cuts through

    # Crossbar rectangle
    horizontal_hole = add_cube(
        "PunchBar",
        size_xyz=(tpost_horizontal_hole_width + 2 * punch_clearance, tpost_length_y, tpost_horizontal_hole_height + 2 * punch_clearance),
        location_xyz=(punch_center_x, 13 + punch_center_y, punch_center_z),
        rotation_xyz=(math.radians(90 - bend1_angle_included / 2), math.radians(0), math.radians(0))
    )

    # Stem rectangle (placed under the bar like a lowercase "t")
    vertical_hole = add_cube(
        "PunchStem",
        size_xyz=(tpost_vertical_hole_width + 2 * punch_clearance, tpost_length_y, tpost_vertical_hole_height + 2 * punch_clearance),
        location_xyz=(punch_center_x, punch_center_y, punch_center_z),
        rotation_xyz=(math.radians(90), math.radians(0), math.radians(0))
        #rotation_xyz=(math.radians(90 - bend1_angle_included / 2), math.radians(0), math.radians(0))
    )
    boolean_diff(bracket, vertical_hole)
    boolean_diff(bracket, horizontal_hole)


for hole in range(4):
    right_hole = add_cylinder(
        "HoleRight{}".format(hole),
        radius=inch(0.125),
        length=100,
        location_xyz=(inch(0.5), -inch(2), inch(2) + inch(1.175) * hole),
        rotation_xyz=(math.radians(90), 0, 0)
    )
    left_hole = add_cylinder(
        "HoleLeft{}".format(hole),
        radius=inch(0.125),
        length=100,
        location_xyz=(inch(2.5), -inch(2), inch(2) + inch(1.175) * hole),
        rotation_xyz=(math.radians(90), 0, 0)
    )
    boolean_diff(bracket, right_hole)
    boolean_diff(bracket, left_hole)

# -----------------------------
# Optional bevel
# -----------------------------
if do_bevel:
    bev = bracket.modifiers.new("Bevel", type="BEVEL")
    bev.width = bevel_width
    bev.segments = bevel_segments
    bev.limit_method = 'ANGLE'
    #bev.angle_limit = math.radians(35)
    bev.use_clamp_overlap = False
    bpy.context.view_layer.objects.active = bracket
    bpy.ops.object.modifier_apply(modifier=bev.name)

API Documentation Links:

https://docs.blender.org/api/current/bpy.ops.mesh.html
https://docs.blender.org/api/current/bmesh.ops.html

Blender Scripting Lesson of the Week: Cylinders

Quick script for creating a cylinder using bpy

import bpy

# Clear all existing objects
for obj in list(bpy.data.objects):
    bpy.data.objects.remove(obj, do_unlink=True)

# Set Units
scene = bpy.context.scene
scene.unit_settings.system = 'METRIC'
scene.unit_settings.scale_length = 0.001  # 1 BU = 1 mm

# Create cylinder
bpy.ops.mesh.primitive_cylinder_add(
    vertices=32, radius=10.0, depth=20.0,
    end_fill_type='NGON', calc_uvs=True,
    enter_editmode=False, align='WORLD',
    location=(0.0, 0.0, -2.0), rotation=(0.0, 0.0, 0.0),
    scale=(1, 1, 1)
)

# Name cylinder
obj = bpy.context.active_object
obj.name = "MyCylinder"

# Frame Selected 
for area in bpy.context.window.screen.areas:
    if area.type == 'VIEW_3D':
        for region in area.regions:
            if region.type == 'WINDOW':
                with bpy.context.temp_override(area=area, region=region):
                    bpy.ops.view3d.view_selected(use_all_regions=False)
                break
        break