From 2e01b69dff7d8060839d707b5e76fa23ad4c80ac Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 28 Oct 2026 00:23:09 +0800 Subject: [PATCH 1/3] refactor(yapper): inject the event DB URI provider; drop campus.auth imports (#970) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of the campus-core carve: campus.yapper is now self-contained — it imports no campus package at all, so the lazy yapper<->auth cycle is gone at the source. create() resolves the event database URI in priority order: db_uri argument > db_uri_provider() callable > YAPPERDB_URI env var > remote campus vault via campus_python. The two branches that lazily imported campus.auth.resources to read the local vault (STORAGE_MODE=1 test mode and DEPLOY=campus.auth) are removed: the auth service now injects a provider backed by its own vault in get_yapper(), and test fixtures export the seeded URI as YAPPERDB_URI env for bare create() calls (the vault secret stays seeded for the vault-delivery contract). Test mode never contacts the remote vault: with no URI resolvable it raises instead of reaching the network (guards issue #319's class of bug). The boundary contract (test_package_boundaries) moves yapper out of SERVICE_LAYERS into SHARED_ALLOWED with zero allowed deps: emitters (auth, api) may import yapper; it must never grow campus dependencies back. New factory unit tests cover the resolution order and assert campus.auth stays unimported at runtime. Verified: import campus.yapper loads only campus.yapper; bare create() under test mode never loads campus.auth. Unit, contract, integration, ruff all green. --- campus/auth/__init__.py | 13 ++- campus/yapper/__init__.py | 144 ++++++++++++++------------ tests/fixtures/yapper.py | 8 +- tests/unit/test_package_boundaries.py | 22 ++-- tests/unit/yapper/test_factory.py | 112 ++++++++++++++++++++ 5 files changed, 220 insertions(+), 79 deletions(-) create mode 100644 tests/unit/yapper/test_factory.py diff --git a/campus/auth/__init__.py b/campus/auth/__init__.py index 18b5585e..ea2ee5cc 100644 --- a/campus/auth/__init__.py +++ b/campus/auth/__init__.py @@ -27,15 +27,24 @@ def get_yapper() -> "YapperInterface": """Get the module-wide yapper instance. - + Initializes yapper lazily if not already initialized. This should be called at app startup to avoid circular dependencies during request handling. + + The event database URI is read from the auth service's own vault + in-process (calling its own HTTP API during initialization would be + circular), so it is injected as a provider — yapper itself has no + campus.auth dependency (#970 phase 2). """ global _yapper_instance if _yapper_instance is None: import campus.yapper - _yapper_instance = campus.yapper.create() + + from .resources import vault + _yapper_instance = campus.yapper.create( + db_uri_provider=lambda: vault["campus.yapper"]["YAPPERDB_URI"] + ) return _yapper_instance diff --git a/campus/yapper/__init__.py b/campus/yapper/__init__.py index ed5deec7..05707b70 100644 --- a/campus/yapper/__init__.py +++ b/campus/yapper/__init__.py @@ -3,14 +3,13 @@ A message broker client for Campus, in Python. This package provides the Yapper class for sending and receiving events. + +yapper is self-contained: it imports no other campus package. Callers +own credential and vault access — configuration arrives through +arguments or environment variables, never by importing campus.auth +(#970 phase 2 removes that lazy import cycle). """ -# This file is required to make this directory a package. -# See https://docs.python.org/3/tutorial/modules.html#packages for more -# information. -# The __all__ variable is used to define the public API of this module. -# See https://docs.python.org/3/tutorial/modules.html#importing-from-a-package -# for more information. __all__ = [ "Event", "EventHandler", @@ -18,40 +17,53 @@ "create", ] +from typing import Callable + from .base import Event, EventHandler, YapperInterface -def create(**kwargs) -> YapperInterface: - """Factory function to get a Yapper client instance. +def create( + *, + db_uri: str | None = None, + db_uri_provider: Callable[[], str] | None = None, + **kwargs, +) -> YapperInterface: + """Factory function returning a backend-specific Yapper client. - # TODO: Update docstring (https://github.com/nyjc-computing/campus/issues/177) + Client identity comes from the environment. The event database URI + is resolved in priority order: + + 1. ``db_uri`` argument + 2. ``db_uri_provider()`` — for callers that own local secret + access (the auth service reads its own vault in-process; + calling its own HTTP API during initialization would be + circular) + 3. ``YAPPERDB_URI`` environment variable + 4. the remote campus vault "campus.yapper" via campus_python Environment variables: CLIENT_ID: Unique identifier for the client (required) CLIENT_SECRET: Client secret for authentication (required) - ENV: Environment type that determines backend ("development", "testing", "staging", "production") - - "development" or "testing": SQLiteYapper - - "staging" or "production": PostgreSQLYapper - YAPPERDB_URI: PostgreSQL connection URI (required for staging/production) - STORAGE_MODE: If "1", use local resources for testing (no external services) + ENV: development | testing | staging | production + STORAGE_MODE: "1" selects the SQLite backend for testing. Test + mode never contacts external services: the URI must arrive + via 1-3 or create() raises. Args: - **kwargs: Backend-specific arguments: - - For SQLite (development/testing): - - db (optional): Database file path, defaults to ":memory:" + db_uri: explicit database URI (highest priority) + db_uri_provider: zero-arg callable returning the database URI + **kwargs: backend-specific arguments (e.g. SQLite ``db`` path) Returns: - YapperInterface: A backend-specific Yapper instance + YapperInterface: SQLiteYapper (STORAGE_MODE=1) or PostgreSQLYapper Raises: - ValueError: If CLIENT_ID or CLIENT_SECRET environment variables are not set, - or if YAPPERDB_URI is not set for PostgreSQL environments + ValueError: If CLIENT_ID or CLIENT_SECRET are unset, if no + database URI can be resolved, or if ENV is not supported. """ # Lazy-import locally to avoid polluting global namespace import os - import campus_python - from .backends.postgres import PostgreSQLYapper from .backends.sqlite import SQLiteYapper @@ -65,54 +77,50 @@ def create(**kwargs) -> YapperInterface: if not client_secret: raise ValueError("CLIENT_SECRET environment variable is required") - # In test mode (STORAGE_MODE=1), use local auth resources directly - # to avoid connecting to external services + yapperdb_uri = db_uri + if yapperdb_uri is None and db_uri_provider is not None: + yapperdb_uri = db_uri_provider() + if yapperdb_uri is None: + yapperdb_uri = os.getenv("YAPPERDB_URI") + if storage_mode == "1": - from campus.auth import resources as auth_resources - yapper_vault = auth_resources.vault["campus.yapper"] - yapperdb_uri = yapper_vault["YAPPERDB_URI"] + if not yapperdb_uri: + raise ValueError( + "STORAGE_MODE=1 (test mode) must not contact external " + "services: provide the event database URI via the db_uri " + "argument, a db_uri_provider callable, or YAPPERDB_URI." + ) yapper = SQLiteYapper(db=yapperdb_uri, client_id=client_id, **kwargs) yapper._init_db() return yapper - # For campus.auth deployment, use local resources to avoid circular dependency - # (service trying to call its own HTTP API during initialization) - deploy = os.getenv("DEPLOY") - if deploy == "campus.auth": - from campus.auth import resources as auth_resources - yapper_vault = auth_resources.vault["campus.yapper"] - yapperdb_uri = yapper_vault["YAPPERDB_URI"] - yapper = PostgreSQLYapper(db_uri=yapperdb_uri, client_id=client_id, **kwargs) - yapper._init_db() - return yapper - - # For other deployments, use campus_python to connect to remote vault - # Create Campus client for vault access - campus = campus_python.Campus(timeout=60) - - # Determine backend based on environment - match env: - case "development" | "testing" | "staging" | "production": - try: - yapper_vault = campus.auth.vaults["campus.yapper"] - yapperdb_uri = yapper_vault["YAPPERDB_URI"] - except Exception as e: - raise ValueError( - f"Failed to retrieve YAPPERDB_URI from vault 'campus.yapper' for {env} environment. " - f"Vault error: {e}. " - f"This could indicate vault service connectivity issues or authentication problems." - ) from e - - if not yapperdb_uri: - raise ValueError( - f"YAPPERDB_URI environment variable is required for {env} environment. " - "Please provide a PostgreSQL connection URI via YAPPERDB_URI." - ) - yapper = PostgreSQLYapper(db_uri=yapperdb_uri, client_id=client_id, **kwargs) - yapper._init_db() - return yapper - - raise ValueError( - f"Unsupported ENV value: {env}. " - "Use 'development', 'testing', 'staging', or 'production'." - ) + if yapperdb_uri is None: + # Resolve from the remote campus vault via campus_python. + import campus_python + + if env not in ("development", "testing", "staging", "production"): + raise ValueError( + f"Unsupported ENV value: {env}. " + "Use 'development', 'testing', 'staging', or 'production'." + ) + campus = campus_python.Campus(timeout=60) + try: + yapper_vault = campus.auth.vaults["campus.yapper"] + yapperdb_uri = yapper_vault["YAPPERDB_URI"] + except Exception as e: + raise ValueError( + f"Failed to retrieve YAPPERDB_URI from vault 'campus.yapper' for {env} environment. " + f"Vault error: {e}. " + f"This could indicate vault service connectivity issues or authentication problems." + ) from e + + if not yapperdb_uri: + raise ValueError( + f"YAPPERDB_URI is required for the {env} environment. " + "Provide a database URI via the db_uri argument, a " + "db_uri_provider callable, or YAPPERDB_URI." + ) + + yapper = PostgreSQLYapper(db_uri=yapperdb_uri, client_id=client_id, **kwargs) + yapper._init_db() + return yapper diff --git a/tests/fixtures/yapper.py b/tests/fixtures/yapper.py index 9b981652..398410b7 100644 --- a/tests/fixtures/yapper.py +++ b/tests/fixtures/yapper.py @@ -3,6 +3,8 @@ Functions for initialising campus.yapper for testing use """ +import os + from . import postgres, require, setup # Module-level variable to track if yapper was initialized @@ -49,7 +51,6 @@ def init(): elif campus.storage.testing.is_test_mode(): # Use a temp file for the database # Note: :memory: doesn't work because each connection creates a new empty DB - import os import tempfile fd, db_path = tempfile.mkstemp(suffix='.sqlite', prefix='yapperdb_') os.close(fd) @@ -60,3 +61,8 @@ def init(): else: db_uri = setup.get_db_uri("yapperdb") yapper_vault["YAPPERDB_URI"] = db_uri + # yapper no longer reads the local vault itself (#970 phase 2): bare + # create() calls in tests (api routes) pick the URI up from the env. + # The vault secret stays for the vault-delivery contract (see + # tests/integration/yapper). + os.environ["YAPPERDB_URI"] = db_uri diff --git a/tests/unit/test_package_boundaries.py b/tests/unit/test_package_boundaries.py index 133306b6..bcb16d94 100644 --- a/tests/unit/test_package_boundaries.py +++ b/tests/unit/test_package_boundaries.py @@ -8,12 +8,15 @@ Rules enforced: -1. Shared layers (common, config, model, storage, webauth, services) may - only depend on the campus subpackages listed in SHARED_ALLOWED. -2. Shared layers must not import a service layer (auth, api, audit, - yapper, flask_campus, deploy) anywhere — module level or lazy. -3. Shared layers must not import flask/werkzeug at module level. The - one documented exception is the emailotp template, which lazily +1. Self-contained layers (common, config, model, storage, webauth, + services, yapper) may only depend on the campus subpackages listed in + SHARED_ALLOWED. yapper's entry is empty: it imports no campus package + at all (its credentials/DB URI arrive via arguments or environment — + #970 phase 2 removed the lazy campus.auth import). +2. Self-contained layers must not import a service layer (auth, api, + audit, flask_campus, deploy) anywhere — module level or lazy. +3. Self-contained layers must not import flask/werkzeug at module level. + The one documented exception is the emailotp template, which lazily imports flask at call time (inside body()/html_body()). 4. campus.common's only module-level third-party import is bcrypt (utils/secret.py). Requests used to ride in via the http client, @@ -41,10 +44,13 @@ "storage": {"common", "model"}, "webauth": {"common", "model"}, "services": {"common"}, + # Self-contained: imports no campus package at all. Emitters (auth, + # api) may import it; it must never grow campus dependencies back. + "yapper": set(), } -# Layer modules that shared layers must never import, even lazily. -SERVICE_LAYERS = {"auth", "api", "audit", "yapper", "flask_campus", "deploy"} +# Layer modules that self-contained layers must never import, even lazily. +SERVICE_LAYERS = {"auth", "api", "audit", "flask_campus", "deploy"} # Third-party modules banned at module level in shared layers. NO_MODULE_LEVEL = {"flask", "werkzeug"} diff --git a/tests/unit/yapper/test_factory.py b/tests/unit/yapper/test_factory.py new file mode 100644 index 00000000..52bef27b --- /dev/null +++ b/tests/unit/yapper/test_factory.py @@ -0,0 +1,112 @@ +"""Factory tests for campus.yapper.create() (#970 phase 2). + +yapper is self-contained: it resolves its event database URI from an +argument, a caller-provided callable, or the environment — and it must +never import campus.auth (that lazy import was the yapper<->auth cycle; +the AST contract test in tests/unit/test_package_boundaries.py guards +it statically). These assertions run in subprocesses so the surrounding +test process cannot mask missing or extra imports, and so environment +variables (STORAGE_MODE, YAPPERDB_URI) are controlled per test. +""" + +import os +import subprocess +import sys +import tempfile +import unittest + + +class TestCreateUriResolution(unittest.TestCase): + def setUp(self): + fd, self.db_path = tempfile.mkstemp(suffix=".sqlite", prefix="yapperfactory_") + os.close(fd) + self.db_path = self.db_path.replace("\\", "/") + + def tearDown(self): + if os.path.exists(self.db_path): + os.remove(self.db_path) + + def _run(self, code: str, **env_overrides) -> str: + env = dict(os.environ) + env.update( + CLIENT_ID="test-client", + CLIENT_SECRET="test-secret", + ENV="testing", + ) + # The outer process (test harness) may set these; isolate. + env.pop("YAPPERDB_URI", None) + env.pop("STORAGE_MODE", None) + env.pop("DEPLOY", None) + env.update(env_overrides) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + check=True, + env=env, + ) + return result.stdout.strip() + + def test_explicit_db_uri_wins(self): + """Test that db_uri argument configures SQLite in test mode.""" + out = self._run( + "import campus.yapper, sys\n" + f"y = campus.yapper.create(db_uri=r'{self.db_path}')\n" + "print(type(y).__name__, 'campus.auth' in sys.modules)\n", + STORAGE_MODE="1", + ) + backend, auth_loaded = out.split() + self.assertEqual(backend, "SQLiteYapper") + self.assertEqual(auth_loaded, "False") + + def test_provider_is_used(self): + """Test that db_uri_provider supplies the URI and campus.auth stays unimported.""" + out = self._run( + "import campus.yapper, sys, os\n" + f"y = campus.yapper.create(db_uri_provider=lambda: r'{self.db_path}')\n" + "print(type(y).__name__, 'campus.auth' in sys.modules," + " os.path.getsize(r'" + self.db_path + "') > 0)\n", + STORAGE_MODE="1", + ) + backend, auth_loaded, db_initialized = out.split() + self.assertEqual(backend, "SQLiteYapper") + self.assertEqual(auth_loaded, "False") + self.assertEqual(db_initialized, "True") + + def test_env_fallback_for_bare_create(self): + """Test that bare create() (api routes) falls back to YAPPERDB_URI env.""" + out = self._run( + "import campus.yapper, sys\n" + "y = campus.yapper.create()\n" + "print(type(y).__name__, 'campus.auth' in sys.modules)\n", + STORAGE_MODE="1", + YAPPERDB_URI=self.db_path, + ) + backend, auth_loaded = out.split() + self.assertEqual(backend, "SQLiteYapper") + self.assertEqual(auth_loaded, "False") + + def test_test_mode_without_uri_raises_before_network(self): + """Test that test mode with no URI raises instead of contacting the remote vault.""" + code = ( + "import campus.yapper\n" + "campus.yapper.create()\n" + ) + env = dict(os.environ) + env.update( + CLIENT_ID="test-client", + CLIENT_SECRET="test-secret", + ENV="testing", + STORAGE_MODE="1", + ) + env.pop("YAPPERDB_URI", None) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + check=False, + env=env, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("STORAGE_MODE=1", result.stderr) + self.assertNotIn("campus.auth", result.stderr) From a77dcd4f230b9eb4ffcb36810f5edd128db270b0 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 28 Oct 2026 06:29:03 +0800 Subject: [PATCH 2/3] docs(yapper): consumer guide for the injected-credentials shape (#970) Adds docs/yapper.md: what the event bus is, the self-contained contract (no campus imports, boundary-enforced), the create() URI resolution order, per-caller recipes (api-style bare create, auth's injected provider, test fixtures), the removed DEPLOY=campus.auth special case, emit/handle usage, and the vault-delivery contract. Also corrects the three package-tree listings that still labelled yapper a 'Logging framework' (it is the event bus), adds yapper to campus/README's Services and Key Components, and records the 'yapper -> (nothing)' dependency rule in PACKAGING.md alongside the other constraints. The stale create() docstring TODO (issue #177, closed long ago) is resolved by the rewritten docstring in the previous commit. --- campus/README.md | 10 ++- docs/PACKAGING.md | 4 ++ docs/architecture.md | 2 +- docs/development-guidelines.md | 2 +- docs/yapper.md | 113 +++++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 docs/yapper.md diff --git a/campus/README.md b/campus/README.md index 9b9d3251..6b341e3e 100644 --- a/campus/README.md +++ b/campus/README.md @@ -22,7 +22,7 @@ campus/ ├── services/ # Email, etc. ├── storage/ # Data persistence ├── integrations/ # External service configurations -└── yapper/ # Logging framework +└── yapper/ # Event bus (self-contained) ``` ### Design Principles @@ -41,6 +41,7 @@ campus/ - **campus.storage**: Backend-agnostic data persistence - **campus.model**: Entity dataclasses (no business logic) - **campus.services**: Email, notifications +- **campus.yapper**: Event bus — self-contained, imports no campus package ([docs/yapper.md](../docs/yapper.md)) ## Key Components @@ -77,6 +78,13 @@ Business logic in `.resources` submodules (e.g., `campus.auth.resources`). - Email notifications - External integrations +### 📨 Yapper (`campus.yapper`) + +- Event bus for domain emissions (`campus.[.].` labels) +- Self-contained: imports no campus package; configuration via arguments/environment +- Database URI resolution: argument → provider callable → `YAPPERDB_URI` env → remote vault +- See [docs/yapper.md](../docs/yapper.md) + ### 🔌 Client Access Use `campus_python` client library: diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index 1520bf7d..3edf78b8 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -34,6 +34,7 @@ weekly → staging → main auth, api → services, storage, model, common storage → common model → common +yapper → (nothing — self-contained) ``` **Key Constraints:** @@ -41,6 +42,9 @@ model → common - `model` contains only entity definitions (minimal dependencies) - `storage` provides backend-agnostic persistence - `common` is self-contained +- `yapper` is self-contained: it imports no campus package; its event + database URI arrives via argument, provider callable, or environment + ([yapper.md](yapper.md)) ## Installation diff --git a/docs/architecture.md b/docs/architecture.md index 66230cf1..6abf6190 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -20,7 +20,7 @@ campus/ ├── services/ # Business services (email, etc.) ├── storage/ # Data persistence layer ├── integrations/ # External service integrations -└── yapper/ # Logging framework +└── yapper/ # Event bus (self-contained; docs/yapper.md) ``` ## Core Principles diff --git a/docs/development-guidelines.md b/docs/development-guidelines.md index 3a426bb9..bdfc6785 100644 --- a/docs/development-guidelines.md +++ b/docs/development-guidelines.md @@ -177,7 +177,7 @@ campus/ ├── services/ # Business services (email, etc.) ├── storage/ # Data persistence layer ├── integrations/ # External service integrations -└── yapper/ # Logging framework +└── yapper/ # Event bus (self-contained) ``` **Key rule:** Business logic lives in `.resources/` submodules. Models (`campus.model`) are pure data structures. diff --git a/docs/yapper.md b/docs/yapper.md new file mode 100644 index 00000000..9c170359 --- /dev/null +++ b/docs/yapper.md @@ -0,0 +1,113 @@ +# campus.yapper — event bus + +`campus.yapper` is Campus's event bus: services emit domain events +(`auth` emits `campus.oauth.token`, `campus.clients.create`, …; `api` +emits submission/assignment/circle events), and clients subscribe and +handle them. Events are persisted in a dedicated event database +(`yapperdb`): PostgreSQL in deployments, a SQLite file in tests. + +This guide covers how to obtain and use a yapper client. For the label +naming convention (shared with journey names, see +[audit-tracing.md](audit-tracing.md)), and for the consumer middleware +that relates emissions to journeys, see the audit tracing doc. + +## Self-contained by contract + +`campus.yapper` imports **no other campus package** — not even +`campus.common`. This is enforced by the boundary contract +(`tests/unit/test_package_boundaries.py`, `SHARED_ALLOWED["yapper"]` +is empty). The reason is historical: yapper used to lazily import +`campus.auth.resources` to read its database URI from the vault, +forming a lazy `yapper ↔ auth` import cycle (#970 phase 2 removed it). + +The practical consequence: **all configuration arrives through +arguments or environment variables.** If a caller has local secret +access (like the auth service reading its own vault), it passes that +access in as a callable instead of yapper reaching into campus.auth. + +## Getting a client: `campus.yapper.create()` + +The event database URI is resolved in priority order: + +| Priority | Source | Notes | +|---|---|---| +| 1 | `db_uri=` argument | Explicit, highest priority | +| 2 | `db_uri_provider()` callable | For callers with local vault access | +| 3 | `YAPPERDB_URI` environment variable | Used by bare `create()` in tests | +| 4 | Remote campus vault `"campus.yapper"` via campus_python | Default for service deployments | + +Client identity always comes from the environment: `CLIENT_ID` and +`CLIENT_SECRET` are required. The backend is `SQLiteYapper` when +`STORAGE_MODE=1` (test mode) and `PostgreSQLYapper` otherwise. + +### Emitters without local vault access (api-style) + +```python +import campus.yapper + +yapper = campus.yapper.create() # creds + vault via environment/SDK +yapper.emit("campus.submissions.submit", {"assignment_id": aid}) +``` + +No arguments needed: credentials come from `CLIENT_ID`/`CLIENT_SECRET`, +and the database URI is fetched from the remote `"campus.yapper"` vault +through campus_python. + +### Callers with local vault access (the auth service) + +A service must not call its own HTTP API during initialization, so the +auth service injects a provider backed by its in-process vault instead +of letting yapper read it (see `campus.auth.get_yapper`): + +```python +import campus.yapper +from .resources import vault + +yapper = campus.yapper.create( + db_uri_provider=lambda: vault["campus.yapper"]["YAPPERDB_URI"] +) +``` + +### Tests + +Test fixtures seed the vault secret **and** export it as the +`YAPPERDB_URI` environment variable (see `tests/fixtures/yapper.py`); +bare `create()` calls then resolve via priority 3. Test mode +(`STORAGE_MODE=1`) never contacts external services: with no URI +resolvable from arguments, provider, or environment, `create()` raises +`ValueError` rather than reaching the remote vault. + +### Removed: the `DEPLOY=campus.auth` special case + +Earlier versions made yapper read the local auth vault itself whenever +`DEPLOY=campus.auth` was set. That branch is gone — auth now passes the +`db_uri_provider` explicitly (above), and yapper no longer consults the +`DEPLOY` variable. No environment changes are needed; the wiring is in +code. + +## Emitting and handling events + +```python +yapper.emit("campus.users.create") # no payload +yapper.emit("campus.clients.update", {"client_id": cid}) # with payload + +@yapper.on_event("campus.clients.update") +def handle(event) -> None: + ... +``` + +Labels follow the `campus.[.].` namespace, which +is shared with journey names. Handlers receive an `Event` (`.label`, +`.data`); `yapper.run()` polls the backend and dispatches to registered +handlers (`start()`/`stop()` control the loop, `listen()` returns raw +events, `subscribe()`/`unsubscribe()` manage label subscriptions). + +## Vault contract + +The event database URI lives as a secret (`YAPPERDB_URI`) in the +`"campus.yapper"` vault, managed by campus.auth with normal client +access grants (see `tests/fixtures/yapper.py` for how test clients are +granted access). The vault delivery contract is covered by +`tests/integration/yapper/test_yapper.py`. Clients should not assume +*how* a yapper instance obtained its URI — only the resolution order +above. From fbd326ec1023af080f9f0516b51f672c85a38efd Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 28 Oct 2026 06:37:21 +0800 Subject: [PATCH 3/3] docs: normalize module docstring names to actual module paths Repo-wide check of all 172 campus modules: every module docstring's first line must be the module's dotted path. Fixes 18 stale ones (legacy pre-reorg paths like campus.apps.api.routes.* and campus.auth.routes.* that survived the oauth_proxy/resources moves, singular/plural drift like routes.booking -> routes.bookings and routes.api_keys -> routes.apikeys, a missing campus. prefix, and a stray .py suffix), normalizes two descriptive-first docstrings (audit.helpers.audit_events, logging_config), and adds the two missing module docstrings (flask_campus.utils, model.booking). Docs module-reference sweep found no stale module paths: the campus.common.http mentions are deliberate history notes on the #976 move, and campus.test / campus.with_user_session in the guides are a test URL and the Campus-client instance method respectively, not modules. --- campus/api/routes/__init__.py | 2 +- campus/api/routes/bookings.py | 2 +- campus/api/routes/circles.py | 2 +- campus/api/routes/emailotp.py | 2 +- campus/audit/helpers/audit_events.py | 2 +- campus/audit/routes/apikeys.py | 2 +- campus/auth/oauth_proxy/__init__.py | 2 +- campus/auth/oauth_proxy/discord/__init__.py | 2 +- campus/auth/oauth_proxy/github/__init__.py | 2 +- campus/auth/oauth_proxy/google/__init__.py | 2 +- campus/auth/resources/access.py | 2 +- campus/auth/resources/user.py | 2 +- campus/auth/resources/vault.py | 2 +- campus/common/utils/utc_time.py | 2 +- campus/flask_campus/login_manager.py | 2 +- campus/flask_campus/parameter.py | 2 +- campus/flask_campus/types.py | 2 +- campus/flask_campus/utils.py | 7 +++++++ campus/logging_config.py | 2 +- campus/model/booking.py | 5 +++++ 20 files changed, 30 insertions(+), 18 deletions(-) diff --git a/campus/api/routes/__init__.py b/campus/api/routes/__init__.py index 175aa3e2..6ea2a55c 100644 --- a/campus/api/routes/__init__.py +++ b/campus/api/routes/__init__.py @@ -1,4 +1,4 @@ -"""campus.apps.api.routes +"""campus.api.routes This is a namespace module for the Campus API routes. """ diff --git a/campus/api/routes/bookings.py b/campus/api/routes/bookings.py index 4127c816..f42c724f 100644 --- a/campus/api/routes/bookings.py +++ b/campus/api/routes/bookings.py @@ -1,4 +1,4 @@ -"""campus.api.routes.booking +"""campus.api.routes.bookings API routes for the Booking resource. """ diff --git a/campus/api/routes/circles.py b/campus/api/routes/circles.py index db0a28f5..98c1393a 100644 --- a/campus/api/routes/circles.py +++ b/campus/api/routes/circles.py @@ -1,4 +1,4 @@ -"""campus.apps.api.routes.circles +"""campus.api.routes.circles API routes for the circles resource. """ diff --git a/campus/api/routes/emailotp.py b/campus/api/routes/emailotp.py index e969276a..a137b48a 100644 --- a/campus/api/routes/emailotp.py +++ b/campus/api/routes/emailotp.py @@ -1,4 +1,4 @@ -"""campus.apps.api.routes.emailotp +"""campus.api.routes.emailotp API routes for the emailotp resource. """ diff --git a/campus/audit/helpers/audit_events.py b/campus/audit/helpers/audit_events.py index dafc1331..94de95fb 100644 --- a/campus/audit/helpers/audit_events.py +++ b/campus/audit/helpers/audit_events.py @@ -1,4 +1,4 @@ -"""Audit event emission helpers for campus.audit. +"""campus.audit.helpers.audit_events Provides helper function and decorator for emitting audit events to the traces table with easy enabling/disabling per route. diff --git a/campus/audit/routes/apikeys.py b/campus/audit/routes/apikeys.py index 674186b3..24217989 100644 --- a/campus/audit/routes/apikeys.py +++ b/campus/audit/routes/apikeys.py @@ -1,4 +1,4 @@ -"""campus.audit.routes.api_keys +"""campus.audit.routes.apikeys API key management endpoints for the audit service. diff --git a/campus/auth/oauth_proxy/__init__.py b/campus/auth/oauth_proxy/__init__.py index 80c3370b..21db08ba 100644 --- a/campus/auth/oauth_proxy/__init__.py +++ b/campus/auth/oauth_proxy/__init__.py @@ -1,4 +1,4 @@ -"""campus.oauth.routes +"""campus.auth.oauth_proxy This module contains the route definitions for Campus and third-party OAuth2 authentication. diff --git a/campus/auth/oauth_proxy/discord/__init__.py b/campus/auth/oauth_proxy/discord/__init__.py index d559e2e0..7aeced79 100644 --- a/campus/auth/oauth_proxy/discord/__init__.py +++ b/campus/auth/oauth_proxy/discord/__init__.py @@ -1,4 +1,4 @@ -"""campus.auth.routes.discord +"""campus.auth.oauth_proxy.discord Routes for Discord OAuth2 Client Credentials flow. diff --git a/campus/auth/oauth_proxy/github/__init__.py b/campus/auth/oauth_proxy/github/__init__.py index c182d599..33d0a3ca 100644 --- a/campus/auth/oauth_proxy/github/__init__.py +++ b/campus/auth/oauth_proxy/github/__init__.py @@ -1,4 +1,4 @@ -"""campus.auth.routes.github +"""campus.auth.oauth_proxy.github Proxy and callback routes for GitHub OAuth2 authentication. """ diff --git a/campus/auth/oauth_proxy/google/__init__.py b/campus/auth/oauth_proxy/google/__init__.py index db7d3e48..2aaf7a50 100644 --- a/campus/auth/oauth_proxy/google/__init__.py +++ b/campus/auth/oauth_proxy/google/__init__.py @@ -1,4 +1,4 @@ -"""campus.auth.routes.google +"""campus.auth.oauth_proxy.google Routes for Google OAuth2. diff --git a/campus/auth/resources/access.py b/campus/auth/resources/access.py index 02327888..1d5358c5 100644 --- a/campus/auth/resources/access.py +++ b/campus/auth/resources/access.py @@ -1,4 +1,4 @@ -"""campus.auth.access +"""campus.auth.resources.access Defines access control utilities for Campus authentication and vault access. """ diff --git a/campus/auth/resources/user.py b/campus/auth/resources/user.py index 3f011707..7bc5c949 100644 --- a/campus/auth/resources/user.py +++ b/campus/auth/resources/user.py @@ -1,4 +1,4 @@ -"""campus.auth.user +"""campus.auth.resources.user Implements Campus API for user access. """ diff --git a/campus/auth/resources/vault.py b/campus/auth/resources/vault.py index f31c463d..6b9d421e 100644 --- a/campus/auth/resources/vault.py +++ b/campus/auth/resources/vault.py @@ -1,4 +1,4 @@ -"""campus.auth.vault +"""campus.auth.resources.vault Implements Campus API for vault access. """ diff --git a/campus/common/utils/utc_time.py b/campus/common/utils/utc_time.py index e428d705..d73f4523 100644 --- a/campus/common/utils/utc_time.py +++ b/campus/common/utils/utc_time.py @@ -1,4 +1,4 @@ -"""campus.common.utils.utc_time.py +"""campus.common.utils.utc_time This module provides time-related utilities for the authentication service, and is meant to replace all usage of the Python `time` module in those modules. diff --git a/campus/flask_campus/login_manager.py b/campus/flask_campus/login_manager.py index cf8ff62d..70f2218b 100644 --- a/campus/flask_campus/login_manager.py +++ b/campus/flask_campus/login_manager.py @@ -1,4 +1,4 @@ -"""flask_campus.login_manager +"""campus.flask_campus.login_manager OAuth Login Manager for Flask integration. """ diff --git a/campus/flask_campus/parameter.py b/campus/flask_campus/parameter.py index 9abcb0f0..9ff45a00 100644 --- a/campus/flask_campus/parameter.py +++ b/campus/flask_campus/parameter.py @@ -1,4 +1,4 @@ -"""campus.common.flask.validation +"""campus.flask_campus.parameter This module provides utilities for validation of arguments against parameters. diff --git a/campus/flask_campus/types.py b/campus/flask_campus/types.py index bc466b63..9b3e0e90 100644 --- a/campus/flask_campus/types.py +++ b/campus/flask_campus/types.py @@ -1,4 +1,4 @@ -"""campus.flask_campus +"""campus.flask_campus.types Common utility functions for validation of flask requests and responses. """ diff --git a/campus/flask_campus/utils.py b/campus/flask_campus/utils.py index a5dc38f2..2800e93f 100644 --- a/campus/flask_campus/utils.py +++ b/campus/flask_campus/utils.py @@ -1,3 +1,10 @@ +"""campus.flask_campus.utils + +Request helpers for downstream Flask apps: user-agent/header/payload +extraction, request unpacking into view signatures, and JSON response +validation. +""" + import inspect import typing from functools import wraps diff --git a/campus/logging_config.py b/campus/logging_config.py index 212d6788..3e37c433 100644 --- a/campus/logging_config.py +++ b/campus/logging_config.py @@ -1,4 +1,4 @@ -"""Logging configuration for Campus application. +"""campus.logging_config This module configures logging for the Campus application, with special attention to OAuth flow debugging. diff --git a/campus/model/booking.py b/campus/model/booking.py index 6f236660..e6ec9d73 100644 --- a/campus/model/booking.py +++ b/campus/model/booking.py @@ -1,3 +1,8 @@ +"""campus.model.booking + +Venue and venue-booking entity models. +""" + from dataclasses import dataclass, field from campus.common import schema