Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@
- **Business logic**: the rules that determine how the domain behaves; validations, decisions, orchestration of use-cases, state transitions, etc.
- **Plumbing**: transport/infrastructure glue (HTTP routing, request parsing, wiring dependencies) that carries inputs to the correct business logic and returns the result.
- **Feature**: a cohesive capability (feedback reporting, player scraping, proxy rotation, etc.) that owns its domain rules, ~~DTOs~~ structs, ports, and integrations. Each feature lives inside a component so it can be reused by multiple bases/projects without duplication.
- **Models** = SQLAlchemy ORM classes mapped to concrete tables (live under `components/bot_detector/database/**/models`). Only the persistence layer (repositories/adapters that talk to storage) should touch them.
- **Structs** = Pydantic data shapes (requests/responses/contracts) shared across components/bases; they live under `components/bot_detector/structs` and replace the old “DTO” term.
- **Models** = SQLAlchemy ORM classes mapped to concrete tables (each feature owns its models under `components/bot_detector/<feature>/database*.py`). Only the persistence layer (repositories/adapters that talk to storage) should touch them.
- **Structs** = Pydantic data shapes (requests/responses/contracts) shared across components/bases; each feature owns its structs (e.g., `components/bot_detector/player/structs.py`) and shared ones live under `bot_detector.core.structs`. They replace the old “DTO” term.
- **Service**: the domain façade exposed by a component (e.g., `PlayerService`). A service orchestrates one cohesive use-case—validation, repositories, messaging, caching—and is what bases call once they finish plumbing work.
- **Repository**: a persistence/adapter layer focused solely on talking to infrastructure (SQLAlchemy, Kafka, S3, HTTP APIs, etc.). Repositories are consumed by services and do not contain orchestration or HTTP-specific logic.
- **Manager**: an infrastructure helper that owns shared resources (Kafka producers, sessions, caches). Managers live alongside plumbing or support libraries and keep long-lived connections healthy.
- **Cache**: lightweight, in-process memoization layers (like `SimpleALRUCache`) that services can use for hot-path data. Caches never contain business logic; they simply store/retrieve entities to reduce load on repositories.

## Working Standards
- **Components** encapsulate each feature’s business logic plus adapters, and may depend on other components/libraries only.
- **Bases** expose public APIs and handle plumbing only (routing, request parsing, dependency wiring) before delegating to components.
- **Components** encapsulate each feature’s business logic plus adapters, and may depend on other components/libraries only. Services may call repositories/adapters but never import FastAPI or base code.
- **Bases** expose public APIs and must stay thin: they handle routing, validation, dependency wiring, and immediately delegate to services. Bases never import ORM models or implement business rules.
- **Projects** only compose bricks + libraries into deployable artifacts; they hold wiring/config, never feature code.
- **Shared structs** (DTOs, interfaces) belong in reusable components like `components/bot_detector/structs` so every base/project can import them without circular dependencies.
- **Shared structs** (DTOs, interfaces) live in reusable modules such as `bot_detector.core.structs`, while feature-specific structs stay within their feature packages. Use the `structs` naming everywhere (no `schemas` leftovers) and suffix types consistently (`FooInput`, `FooResponse`, etc.).
- **Tests** live under the workspace-level `test/` directory via `[tool.polylith.test]`, so base/component fixtures and contract tests should be added there rather than inside each brick folder. Add per-base `resources/` directories only when a base needs static assets or config that isn’t shared elsewhere.

# The Polylith Architecture
Expand Down Expand Up @@ -125,4 +129,4 @@ find . -type f -name "pyproject.toml" -not -path "*/.venv/*" -execdir sh -c 'ech
# syncing in all directories, so uv cache is setup
```sh
find . -type f -name "pyproject.toml" -not -path "*/.venv/*" -execdir sh -c 'echo "🔄 syncing in $(pwd)"; uv sync' \;
```
```
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from bot_detector.api_public.src.core.config import DB_SEMAPHORE, settings
from bot_detector.database import Settings as DBSettings
from bot_detector.database import get_session_factory
from bot_detector.api_public.core.config import DB_SEMAPHORE, settings
from bot_detector.core.database import Settings as DBSettings, get_session_factory
from sqlalchemy.ext.asyncio import AsyncSession

# Reuse the shared database component instead of maintaining copy
Expand Down
Original file line number Diff line number Diff line change
@@ -1,79 +1,80 @@
import logging
from contextlib import asynccontextmanager

from bot_detector.api_public.src import api
from bot_detector.api_public.src.core.fastapi.dependencies.kafka import kafka_manager
from bot_detector.api_public.src.core.fastapi.middleware import (
LoggingMiddleware,
PrometheusMiddleware,
)
from bot_detector.kafka import Settings as KafkaSettings
from bot_detector.kafka.repositories import RepoReportsToInsertProducer
from fastapi import FastAPI
from fastapi.middleware import Middleware
from fastapi.middleware.cors import CORSMiddleware
from prometheus_client import start_http_server

logger = logging.getLogger(__name__)


def init_routers(_app: FastAPI) -> None:
_app.include_router(api.router)


def make_middleware() -> list[Middleware]:
middleware = [
Middleware(
CORSMiddleware,
allow_origins=[
"http://osrsbotdetector.com/",
"https://osrsbotdetector.com/",
"http://localhost",
"http://localhost:8080",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
),
Middleware(LoggingMiddleware),
Middleware(PrometheusMiddleware),
]
return middleware


@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("startup initiated")
kafka_manager.set_producer(
key="reports_to_insert",
producer=RepoReportsToInsertProducer(
bootstrap_servers=KafkaSettings().KAFKA_BOOTSTRAP_SERVERS
),
)
producer = kafka_manager.get_producer(key="reports_to_insert")
await producer.start()
yield
await producer.stop()
logger.info("shutdown completed")


def create_app() -> FastAPI:
_app = FastAPI(
title="Bot-Detector-API",
description="Bot-Detector-API",
middleware=make_middleware(),
lifespan=lifespan,
)
init_routers(_app=_app)
return _app


app = create_app()


start_http_server(8000)


@app.get("/")
async def root():
return {"message": "Hello World"}
import logging
from contextlib import asynccontextmanager

from bot_detector.kafka import Settings as KafkaSettings
from bot_detector.kafka.repositories import RepoReportsToInsertProducer
from fastapi import FastAPI
from fastapi.middleware import Middleware
from fastapi.middleware.cors import CORSMiddleware
from prometheus_client import start_http_server

from bases.bot_detector.api_public import routes
from bases.bot_detector.api_public.core.fastapi.dependencies.kafka import kafka_manager
from bases.bot_detector.api_public.core.fastapi.middleware import (
LoggingMiddleware,
PrometheusMiddleware,
)

logger = logging.getLogger(__name__)


def init_routers(_app: FastAPI) -> None:
_app.include_router(routes.router)


def make_middleware() -> list[Middleware]:
middleware = [
Middleware(
CORSMiddleware,
allow_origins=[
"http://osrsbotdetector.com/",
"https://osrsbotdetector.com/",
"http://localhost",
"http://localhost:8080",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
),
Middleware(LoggingMiddleware),
Middleware(PrometheusMiddleware),
]
return middleware


@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("startup initiated")
kafka_manager.set_producer(
key="reports_to_insert",
producer=RepoReportsToInsertProducer(
bootstrap_servers=KafkaSettings().KAFKA_BOOTSTRAP_SERVERS
),
)
producer = kafka_manager.get_producer(key="reports_to_insert")
await producer.start()
yield
await producer.stop()
logger.info("shutdown completed")


def create_app() -> FastAPI:
_app = FastAPI(
title="Bot-Detector-API",
description="Bot-Detector-API",
middleware=make_middleware(),
lifespan=lifespan,
)
init_routers(_app=_app)
return _app


app = create_app()


start_http_server(8000)


@app.get("/")
async def root():
return {"message": "Hello World"}
24 changes: 24 additions & 0 deletions bases/bot_detector/api_public/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from fastapi import APIRouter

from bases.bot_detector.api_public.routes.v2.player import router as player_router
from bases.bot_detector.api_public.routes.v2.reports import router as reports_router
from bases.bot_detector.api_public.routes.v2.feedback import router as feedback_router
from bases.bot_detector.api_public.routes.v2.labels import router as labels_router


def _build_v2() -> APIRouter:
api_router = APIRouter(prefix="/v2")
for feature_router in (
player_router,
reports_router,
feedback_router,
labels_router,
):
api_router.include_router(feature_router)
return api_router


router = APIRouter()
router.include_router(_build_v2())

__all__ = ["router"]
24 changes: 24 additions & 0 deletions bases/bot_detector/api_public/routes/v2/feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import logging

from bot_detector.feedback.services import FeedbackService
from bot_detector.feedback.structs import FeedbackInput
from bot_detector.core.structs.responses import Ok
from bases.bot_detector.api_public.core.fastapi.dependencies.session import get_session
from bases.bot_detector.api_public.core.fastapi.dependencies.to_jagex_name import to_jagex_name
from fastapi import APIRouter, Depends, HTTPException, status

router = APIRouter(tags=["Feedback"])
logger = logging.getLogger(__name__)


@router.post("/feedback", response_model=Ok, status_code=status.HTTP_201_CREATED)
async def post_feedback(
feedback: FeedbackInput,
session=Depends(get_session),
):
repo = FeedbackService(session)
feedback.player_name = await to_jagex_name(feedback.player_name)
success, detail = await repo.insert_feedback(feedback=feedback)
if not success:
raise HTTPException(status_code=422, detail=detail)
return Ok(detail=detail)
32 changes: 32 additions & 0 deletions bases/bot_detector/api_public/routes/v2/labels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging

from bot_detector.labels.services import LabelService
from bot_detector.labels.structs import LabelResponse
from bases.bot_detector.api_public.core.fastapi.dependencies.session import get_session
from fastapi import APIRouter, Depends, status

router = APIRouter(tags=["Labels"])
logger = logging.getLogger(__name__)


@router.get("/labels", response_model=list[LabelResponse], status_code=status.HTTP_200_OK)
async def get_labels(session=Depends(get_session)):
repo = LabelService(session)
labels = await repo.get_labels()
_labels = []
for label in labels:
res = LabelResponse(**label.__dict__)
res.label = res.label.lower()
_labels.append(res)
return _labels


@router.get("/labels/{label_id}", response_model=LabelResponse | None)
async def get_label_by_id(label_id: int, session=Depends(get_session)) -> LabelResponse | None:
repo = LabelService(session)
label = await repo.get_label_by_id(label_id=label_id)
if label is None:
return None
res = LabelResponse(**label.__dict__)
res.label = res.label.lower()
return res
Loading