src/bx_skills/catalog_skills/bx-python-clean-architecture/SKILL.md
Use when structuring Python 3.10+ projects with layered ports-and-adapters architecture, reviewing layer dependency violations, scaffolding domain-driven designs, deciding where code belongs across domain, application, adapter, and composition layers, or setting up ports, UoW, outbox, and idempotency patterns
npx skillsauth add bitranox/bx_skills bx-python-clean-architectureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Framework-agnostic, typed Python architecture optimized for change, testability, and clear boundaries. Inner layers never import from outer layers. Domain stays pure.
Primary goal: Keep the cost of change low over the lifetime of the system. Business logic should be easy to understand, test, and evolve independently of infrastructure choices.
Target: Python >=3.10 | Typed | Pure domain | Framework-agnostic core | No compatibility shims
When NOT to use:
script-mode.md)Inner layers never import from outer layers. Source code dependencies point inward only, toward more stable and abstract code.
Inner layers change less frequently and for more important reasons. Outer layers (I/O, formatting, frameworks) are plugins to the inner layers.
+------------------------------------------------------------+
| Composition Root (outermost) |
| compose.py, main entrypoints, framework wiring |
| +------------------------------------------------------+ |
| | Adapters | |
| | Route handlers, ORM repositories, Pydantic models, | |
| | message consumers, CLI parsers, HTTP clients | |
| | +------------------------------------------------+ | |
| | | Application | | |
| | | Use case classes, Protocol ports, | | |
| | | request/response dataclasses | | |
| | | +------------------------------------------+ | | |
| | | | Domain | | | |
| | | | @dataclass(frozen=True) entities, | | | |
| | | | Value Objects, Domain Services, | | | |
| | | | Domain Events | | | |
| | | +------------------------------------------+ | | |
| | +------------------------------------------------+ | |
| +------------------------------------------------------+ |
+------------------------------------------------------------+
You may need more or fewer layers depending on complexity. The dependency rule always applies: dependencies point inward, abstraction increases inward.
| Layer | Contains | Rules |
|----------------------|------------------------------------------|-----------------------------------------------------------------------------|
| Domain | Entities, Value Objects, Domain Services | Pure, sync, no I/O, no logs, no frameworks |
| Application | Use Cases, Ports (Protocols), DTOs | Orchestrates domain + ports; no framework types |
| Adapters | Transport, Persistence, Messaging | Implements ports; maps DTOs <-> external formats |
| Composition Root | Wiring | Binds adapters to ports per process; the only place that imports everything |
Data passed across a boundary must always be in the form most convenient for the inner layer. Never pass ORM row structures or Pydantic request models inward -- adapters convert to domain types at the boundary.
| | Domain | Use Cases |
|------------------|-------------------------------------------------------------|--------------------------------------------------------------------|
| Scope | Core business rules (exist without automation) | Application-specific workflows (exist because system is automated) |
| Change rate | Slowest | Faster (new features, workflow changes) |
| Dependencies | Know nothing about use cases | Depend on domain, never the reverse |
| Python | @dataclass(frozen=True, slots=True) with business methods | Classes in application/use_cases/ with execute() |
Use case request/response models must be independent -- no framework types, no entity references, no knowledge of delivery mechanism.
Your top-level directory structure should tell readers about the system, not the framework. When new developers look at the project, their first impression should be "This is a lending platform" -- not "This is a FastAPI app."
Bad -- framework-centric:
src/api/routes/ middleware/ dependencies/ models/ schemas/ crud/
Good -- domain-centric:
src/lending_platform/
loans/
domain/ # Loan entity, Money VO, interest rules
application/
use_cases/ # create_loan.py, approve_loan.py
ports/ # loan_repository.py, credit_check.py
adapters/ # sqlalchemy_loan_repo.py, experian_credit.py
customers/
domain/ application/ adapters/
composition/ # compose.py -- FastAPI only appears here
| Question | Good answer | |-------------------------------------------------------------|-------------------------------------------| | Can you tell what the system does from top-level folders? | Yes -- bounded contexts visible | | Can you tell what framework is used? | No -- hidden in adapters/composition | | Can you unit-test all use cases without any framework? | Yes -- plain dataclasses + protocol ports | | Could you swap the delivery mechanism (web -> CLI)? | Yes -- add adapter, zero core changes |
Brief, practical guidelines for structuring modules in Python layered architecture.
One actor per module. Each use case serves one stakeholder group. If two different teams need changes to the same module, split it. Use a Facade if callers need a single entry point.
Extend by adding, not by modifying. New behavior should mean new adapters implementing existing ports, not editing use cases. New output format? New adapter -- zero changes to the use case.
Honor port contracts. All adapters implementing a port must satisfy the same behavioral contract. Verify via contract tests -- a single parameterized test suite run against every adapter implementation (in-memory, PostgreSQL, Redis). If any adapter needs special handling, the port contract is wrong.
Keep ports narrow. A UserRepository port with find, save, delete, search, bulk_import, export_csv serves too many consumers. Split into focused ports: UserReader(Protocol), UserWriter(Protocol). Each use case depends only on the port slice it needs.
Depend on abstractions for volatile code. Domain and application layers import only Protocols and dataclasses. Stable things (stdlib, well-established libraries) are fine to depend on directly. The composition root is the only place that imports concrete adapter classes.
Prefer immutability in the domain. Use @dataclass(frozen=True, slots=True) for entities and value objects. Push mutable state to adapters where it belongs, protected by the Unit of Work pattern.
Use Protocol to invert dependencies. When the natural direction of a function call opposes the desired dependency direction, introduce a Protocol in the inner layer and implement it in the outer layer. This is what makes the dependency rule possible.
External Input -> Pydantic (validate) -> Dataclass (domain) -> Pydantic (serialize) -> Output
(raw) (adapter boundary) (pure business logic) (adapter boundary) (JSON/API)
| Use Case | Type | Notes |
|---------------------|---------------------------------------|--------------------------------------|
| Domain entities/VOs | @dataclass(frozen=True, slots=True) | Pure, no deps |
| Internal DTOs | @dataclass | When data already trusted |
| Boundary validation | Pydantic BaseModel | Parse untrusted input, coerce types |
| Serialization | Pydantic BaseModel | .model_dump(), .model_validate() |
| Configuration | Pydantic BaseSettings | Env var parsing |
Never: Pass dict[str, Any] between modules or layers. Dicts acceptable only when: truly dynamic data, <3 keys, single function scope, no business logic.
class PaymentPort(Protocol):
async def charge(self, customer_id: str, amount: Money) -> str: ...
Ports are Protocols in application/ports/. Adapters implement them. The business rules own the interface; adapters are plugins. See port-contracts.md for all standard port definitions (UoW, Outbox, IdempotencyStore, IdProvider, Clock).
class UnitOfWork(Protocol[D]):
async def run(self, fn: Callable[[D, RequestContext], Awaitable[T]], *, ctx: RequestContext | None = None) -> T: ...
| Pattern | Rule |
|-------------------|----------------------------------------------------------------|
| Lock ordering | Sort IDs before acquiring locks (prevents deadlocks) |
| Outbox | Persist events within same transaction; publish after commit |
| Idempotency | run_once(key, fn) with uniqueness guard |
| Timeouts | asyncio.wait_for(coro, timeout=); propagate CancelledError |
The database and UI are plugins to the business rules. Core business rules are kept separate from, and independent of, components that are either optional or implemented in many different forms. All dependency arrows point inward.
# Business rules own the interface (port)
class WikiPageRepository(Protocol):
def find(self, slug: str) -> WikiPage | None: ...
def save(self, page: WikiPage) -> None: ...
# Adapters are plugins -- each implements the same port
class InMemoryWikiPageRepo: ... # development/testing plugin
class FileSystemWikiPageRepo: ... # flat-file plugin
class PostgresWikiPageRepo: ... # production plugin
The boundary sits at the Protocol. Business rules know nothing about which adapter is plugged in.
| Mode | Mechanism | Cost | Python example |
|-----------------------------|-------------------------------------|------------|-------------------------------------------|
| Source-level (monolith) | Function calls, same process | Negligible | Standard import + Protocol |
| Deployment component | Separate pip packages, same process | Negligible | myapp-domain, myapp-adapters-postgres |
| Local process | Sockets / IPC | Moderate | FastAPI + Celery on same host |
| Service | Network (HTTP/gRPC/messaging) | High | Separate deployments |
Recommended approach: Start at source-level. Push decoupling to where a service could be formed, but keep components in the same process. Escalate to deployment-level or service-level only when development, deployment, or operational issues demand it.
At runtime, boundary crossing is just a function call. The trick is managing source code dependencies so they always point toward the inner layer. Use Protocol to invert dependencies when control flow opposes the desired dependency direction.
Full boundaries are expensive. When the cost is too high, use a partial strategy:
| Strategy | Inverted? | When to use |
|------------------------|---------------|------------------------------------------------------------------------------------|
| Skip the Last Step | Yes | Full interface design, but keep both sides in same package. Expect to split later. |
| Strategy Pattern | One direction | Single Protocol with concrete implementation. Most common in Python. |
| Facade | No | Thin class grouping service calls. Unlikely to ever need full boundary. |
When to draw boundaries: Watch for friction. Implement boundaries at the inflection point where the cost of implementing becomes less than the cost of ignoring. Review boundary decisions as the system evolves.
| Signal | Action |
|------------------------------------------------|-------------------------------------------|
| Two modules change for different reasons/rates | Draw boundary |
| You want to swap an implementation | Draw boundary (introduce Protocol) |
| Cross-team ownership | Draw boundary (independent deployability) |
| Single developer, no swap foreseeable | Skip or use partial boundary |
Split behaviors that are hard to test from behaviors that are easy to test into two modules. The I/O-facing module contains hard-to-test behavior stripped to its barest essence; the logic module contains all extracted testable logic.
| Boundary | I/O Side (thin) | Logic Side (testable) | |-----------------------|--------------------------------------|--------------------------------| | UI | View (template/renderer) | Presenter (formats View Model) | | Database | Repository adapter (SQL/ORM) | Use case + repository port | | External services | HTTP client adapter | Use case + service port | | Message queues | Consumer adapter (deserialize + ack) | Use case + event handler |
Presenter example:
@dataclass
class LoanViewModel:
principal_display: str # "$1,234.56"
rate_display: str # "4.50%"
is_overdue: bool
status_label: str # "Active" | "Defaulted"
class LoanPresenter:
def present(self, loan: Loan, today: date) -> LoanViewModel:
return LoanViewModel(
principal_display=f"${loan.principal.to_decimal():,.2f}",
rate_display=f"{loan.rate * 100:.2f}%",
is_overdue=loan.next_due_date < today,
status_label="Defaulted" if loan.is_defaulted else "Active",
)
# View just renders the ViewModel -- zero logic, strings/bools only
Principle: At every architectural boundary, look for the opportunity to isolate I/O. Extract testable behavior into a module that works with simple data structures and protocol ports.
| Layer | Style |
|---------------------|---------------------------------------------------------------------------------------------|
| Domain/Application | Raise domain exceptions |
| Adapters/Boundaries | Catch -> map to result envelope {"ok": False, "error": {"code": "...", "message": "..."}} |
| Transport | Map -> HTTP status / exit codes |
Never let raw exceptions leak to external consumers.
src/<pkg>/
<bounded_context>/
domain/ # entities.py, values.py, events.py, services.py
application/
use_cases/
ports/ # uow.py, outbox.py, idempotency.py, id_provider.py, clock.py, <entity>_repository.py
adapters/
memory/ # in-memory implementations
uow/
platform/ # config.py, observability.py
composition/ # compose.py (wiring)
testing/ # Testing API (NOT deployed to production)
Flatten if single small domain; re-introduce <bounded_context>/ on growth.
Guardrails: domain imports nothing outside domain. application imports domain and its own ports. adapters implement application.ports and map to/from domain DTOs. Composition wires everything.
Each package directory requires an __init__.py (can be empty). Omit only if using implicit namespace packages (PEP 420).
Keep packages cohesive and their dependency graph clean:
common/ packages into focused ones: common_types, common_testing, common_infra.Protocol in the inner layer or extracting shared types into a new package.Protocols and base types. Packages with few dependents can be concrete and change freely.package-private. Use import-linter contracts, _-prefixed modules, __all__ in __init__.py, and leading-underscore convention to enforce boundaries. Organization without encapsulation is just folder structure, not architecture.Four packaging approaches from least to most robust:
| Approach | Encapsulation | Recommended? | |----------------------------------------------|---------------|-------------------------------| | By Layer (web/, service/, data/) | Weak | No -- prototype only | | By Feature (orders/, billing/) | Moderate | Small projects | | Ports & Adapters (domain/, adapters/) | Strong | Default choice | | By Component (single facade per feature) | Best | Monolith-to-microservice path |
Bounded contexts must not share domain internals. Choose one integration style per boundary:
| Style | When | Mechanism |
|---------------------------|---------------------------------------------|-------------------------------------------------------------|
| Domain Events | Eventual consistency acceptable | Outbox -> message broker -> consumer adapter |
| Application Service | Synchronous query needed | Context A's adapter calls Context B's use case via port |
| Shared Kernel | Tight coupling justified (e.g., Money VO) | Shared kernel/ package; both contexts depend inward on it |
| Anti-Corruption Layer | Integrating legacy/external systems | Adapter translates external model to local domain model |
Rules:
context_a.domain and context_b.domainreview-checklists.md)Separate things that change for different reasons: UI concerns change independently from business rules, which change independently from persistence details.
Use cases are narrow vertical slices through all layers. Each use case changes at a different rate, for different reasons. Keep them separate: one module per use case in application/use_cases/.
| Mode | Communication | When to escalate | |-----------------------------|---------------------------------|--------------------------------------------| | Source-level (monolith) | Direct calls | Default starting point | | Deployment-level | Same process, separate packages | When teams need independent release cycles | | Service-level | Network calls | When operational scaling demands it |
Start source-level. A good architecture allows sliding up and back down this spectrum without rewriting.
| Type | Definition | Action | |----------------|------------------------------------------------------------|---------------------------------------| | True | Every change to one requires the same change to the other | Eliminate -- extract shared code | | Accidental | Code looks similar now but evolves along different paths | Do not unify -- they will diverge |
Common traps: Two use cases with similar DTOs (accidental -- each serves different actors). A DB record that looks like a view model (accidental -- keep layers decoupled). Two contexts with similar User types (accidental -- will evolve independently). Rule: When separating use cases or layers, similar code is usually accidental duplication. Verify before unifying.
The examples use async throughout. Adapt based on I/O profile:
| Profile | Style | Notes |
|---------------------------------------------|--------------------------------|---------------------------------------------|
| Network I/O dominant (APIs, DBs, messaging) | async | Use asyncio, async def, await |
| CPU-bound or simple CLI tools | Sync | Drop async/await; use regular functions |
| Mixed | async with run_in_executor | Keep domain pure either way |
Domain stays the same regardless: pure, sync, no I/O. The choice affects application and adapter layers only. Ports can define sync or async methods -- pick one per project and stay consistent.
| Type | Purpose | |-----------------|-------------------------------------------------------------| | Unit | Domain + use cases with in-memory adapters | | Contract | Same suite against all port implementations (parameterized) | | Integration | Real infra via composition root | | E2E | Public surface (HTTP/CLI) |
Tests sit in the outermost layer -- nothing depends on them, but they depend inward. Tests that mirror production structure 1:1 (test class per production class) are structurally coupled and fragile. Refactoring production code breaks hundreds of tests.
Solution: Test through a Testing API -- a dedicated API that hides the structure of the application from tests.
The Testing API has superpowers: seed data directly, freeze time, bypass auth, force testable states. It is a superset of the application's use cases.
# testing/api.py -- NOT deployed to production
class TestingAPI:
def __init__(self, app: Application) -> None:
self._app = app
async def seed_user(self, user_id: str, name: str, role: str = "admin") -> User:
"""Bypass registration, create user directly."""
...
def freeze_time(self, at: datetime) -> None:
"""Replace clock port with frozen clock."""
self._app.clock = FrozenClock(at)
async def execute_as(self, user_id: str, use_case, input):
"""Execute any use case as any user, bypassing auth."""
ctx = RequestContext(user_id=user_id, roles=("admin",))
return await use_case.execute(input, ctx=ctx)
# tests/conftest.py
@pytest.fixture
async def api() -> TestingAPI:
app = create_test_app() # In-memory adapters, no real infra
return TestingAPI(app)
Test through behavior, not structure:
# GOOD: Tests business rules through the Testing API
class TestPlaceOrder:
async def test_valid_order_succeeds(self, api: TestingAPI):
await api.seed_user("u1", "Alice")
order_id = await api.execute_as("u1", api.place_order, PlaceOrderCommand(...))
assert order_id is not None
Keep the Testing API and its fakes/builders in a separate testing/ package that is never included in production builds.
Frameworks want deep integration. Protect your core by keeping them in outer layers only.
| Risk | Description | |--------------------------|-------------------------------------------------------------------------| | Architecture pollution | Framework asks you to inherit base classes into your entities/use cases | | Outgrowing the framework | As your product matures, the framework's design fights you | | Evolution divergence | Framework deprecates features you depend on | | Lock-in | A better alternative appears, but you're stuck |
| Principle | Python application |
|----------------------------------------------------|------------------------------------------------------------------------------------------------|
| Keep at arm's length | Framework imports only in adapters/ and composition/, never in domain/ or application/ |
| Don't derive business objects from framework bases | Entities are plain @dataclass, not Django Model or SQLAlchemy Base |
| Use proxies in outer layers | Adapter classes wrap framework behavior and implement your ports |
| Let only composition root know the framework | compose.py wires FastAPI/Django; it's the dirtiest module -- that's OK |
# WRONG: Framework in domain
class Order(DeclarativeBase): ... # Entity married to SQLAlchemy
# RIGHT: Framework stays in adapters
# domain/entities.py
@dataclass(frozen=True, slots=True)
class Order: ...
# adapters/persistence/models.py
class OrderModel(DeclarativeBase):
def to_domain(self) -> Order: ...
@classmethod
def from_domain(cls, order: Order) -> "OrderModel": ...
Frameworks you must depend on (stdlib, typing, dataclasses): that's fine -- but it should be a decision, not an accident.
The composition root is the outermost wiring point. Nothing depends on it -- it depends on everything.
Responsibilities: Create factories/strategies, wire concrete adapters to abstract ports, hand control to the application.
You can have multiple composition roots for different environments:
# composition/main_prod.py
def create_app() -> Application:
config = ProdConfig.from_env()
return Application(uow=PostgresUoW(config.db_url), cache=RedisCache(config.redis_url))
# composition/main_test.py
def create_app() -> Application:
return Application(uow=InMemoryUoW(), cache=InMemoryCache())
# composition/main_dev.py
def create_app() -> Application:
return Application(uow=SqliteUoW(":memory:"), cache=InMemoryCache())
The composition root is deliberately dirty. It imports every concrete class so the rest of the system stays abstract. Keep it thin: parse config, construct, wire, start. No business logic.
Architecture is defined by boundaries and dependencies, not by process boundaries. Microservices are a deployment choice, not an architectural choice.
| Fallacy | Reality | |-------------------------------------------|------------------------------------------------------------------------| | "Services are decoupled" | Still strongly coupled by shared data structures | | "Services enable independent development" | Only if internal architecture is clean; monoliths can achieve the same | | "Adding a new feature is easy" | Cross-cutting features require coordinated changes across all services |
Rule: Start monolith with clean internal boundaries. Extract services only when you have a proven need for independent deployment. A well-structured monolith is architecturally superior to poorly structured microservices.
| Detail | Why it's not architecture | Python implication | |----------------|----------------------------------------------------------|------------------------------------------------------------------| | Database | Just a mechanism to move data between disk and RAM | Repository ports return domain dataclasses, never ORM models | | Web / UI | An I/O device -- one of many delivery mechanisms | Route handlers are thin adapters; no business logic in routes | | Frameworks | Tools for someone else's problems, not your architecture | Use in adapters/composition only; domain/application import none |
Key distinction: The data is architecturally significant. The database is not.
trace_id via contextvars/health) and readiness (/ready) checks when transport existsos.environ outside itRequestContext (claims/roles) to use casesimport-linter)trace_id via contextvars)UnitOfWork[D]); no Mapping[str, Any] for depsDomainEvent base TypedDict| Mode | Output | Reference |
|--------------|-----------------------------------------------------------|----------------------------|
| GENERATE | Full domain-first project with tests | See canonical-example.md |
| REVIEW | Violations + PR-ready fix checklist | See review-checklists.md |
| LIBRARY | Small public API, py.typed, deprecation policy, plugins | See library-mode.md |
| SCRIPT | Single file with logical layer sections | See script-mode.md |
The agent selects the mode based on context:
| Signal | Mode |
|---------------------------------------------------|----------|
| "Create a new project/service/feature" | GENERATE |
| "Review this code for architecture violations" | REVIEW |
| "Build a reusable library/SDK/package" | LIBRARY |
| Deliverable is a single file; no pyproject.toml | SCRIPT |
If ambiguous, ask the user. A project may use multiple modes over its lifecycle (GENERATE initially, REVIEW on changes, LIBRARY if extracted).
[tool.importlinter]
root_package = "<pkg>"
[[tool.importlinter.contracts]]
name = "Clean layers"
type = "layers"
layers = ["<pkg>.domain", "<pkg>.application", "<pkg>.adapters"]
[[tool.importlinter.contracts]]
name = "No cross-context imports"
type = "independence"
modules = ["<pkg>.orders", "<pkg>.billing", "<pkg>.shipping"]
Multi-context: add per-context layering + independence contracts. See review-checklists.md.
.../domain/application/Protocols) for external needs| Mistake | Fix |
|------------------------------------------|---------------------------------------------------------------------------------------------|
| Importing ORM models in domain | Domain uses plain dataclasses; adapters map to/from ORM |
| Pydantic models in domain layer | Pydantic at boundaries only; domain uses @dataclass(frozen=True, slots=True) |
| Use case returns framework response | Use case returns DTO; adapter maps to HTTP/CLI response |
| Passing dict[str, Any] between layers | Define typed dataclasses or Pydantic models at each boundary |
| Raw DB connections in use cases | Use UoW pattern; use case receives transaction-bound repos |
| Logging in domain entities | Domain stays pure; observability hooks at adapter boundaries |
| Reading os.environ in core | Centralize config parsing; inject via composition root |
| Framework types leaking into ports | Ports use stdlib types only (Protocol, dataclass, TypedDict) |
| Unifying accidentally similar DTOs | Each use case gets its own request/response -- they serve different actors and will diverge |
| Passing entities across boundaries | Use separate DTOs; entities and view models change for different reasons |
| Folder structure reveals framework | Organize by bounded context, not by framework convention |
| Starting with microservices | Start monolith with clean boundaries; extract services when proven need exists |
| Tests structurally coupled to production | Test through a Testing API that hides internal structure |
| Premature boundary elimination | Verify duplication is real before unifying; similar code often serves different actors |
| File | Content |
|------------------------|---------------------------------------------------------------------|
| port-contracts.md | All standard Protocol definitions with full code |
| canonical-example.md | Complete Money Transfer example (domain through composition) |
| library-mode.md | Library/SDK development: public API, versioning, plugins, packaging |
| script-mode.md | Single-file scripts: PEP 723, exit codes, logical layout |
| review-checklists.md | All review checklists for REVIEW mode output |
| Term | Definition |
|-------------------------|---------------------------------------------------------------------------------------------|
| Adapter | Concrete implementation of a port (transport/persistence/messaging); a plugin to the core |
| Application Layer | Use cases + port contracts (no framework types) |
| Boundary | A line separating software elements; restricts knowledge between sides |
| Composition Root | Process bootstrap wiring adapters to ports; the outermost wiring point |
| Domain | Pure business logic (entities, value objects, domain services); the most stable layer |
| DomainEvent | Base TypedDict all domain events extend (type, v, id, at) |
| Outbox | Transactional event persistence for reliable publish-after-commit |
| Partial Boundary | Lightweight boundary (strategy, facade) when full separation is too expensive |
| Plugin Architecture | External details (DB, UI) are plugins to business rules via ports |
| Port | Core-owned interface (Protocol) expressing an external dependency |
| Testing API | Test-only API with superpowers (seed, freeze time, bypass auth) hiding production structure |
| UoW | Port scoping a transaction, yielding tx-bound adapters to a work function |
development
Textual TUI framework documentation reference
testing
Use when creating new skills, editing existing skills, structuring SKILL.md files, writing skill frontmatter, testing skills with subagents, deploying skills, or verifying skills work before deployment
development
Use when building transparent remote procedure calls, distributed computing, or remote object proxying in Python with RPyC. Use when asked about rpyc.connect, rpyc.Service, netref proxies, async_(), BgServingThread, SSLAuthenticator, DeployedServer, or rpyc_classic.py.
tools
Use when choosing Python libraries for a task, when writing new Python code that needs dependencies, when reviewing Python imports for non-preferred libraries, or when unsure which library to use for JSON, HTTP, logging, TOML, YAML, compression, database access, testing, or CLI tools