Skip to content
Merged
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
10 changes: 9 additions & 1 deletion campus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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.<subject>[.<sub>].<action>` 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:
Expand Down
2 changes: 1 addition & 1 deletion campus/api/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.apps.api.routes
"""campus.api.routes

This is a namespace module for the Campus API routes.
"""
Expand Down
2 changes: 1 addition & 1 deletion campus/api/routes/bookings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.api.routes.booking
"""campus.api.routes.bookings

API routes for the Booking resource.
"""
Expand Down
2 changes: 1 addition & 1 deletion campus/api/routes/circles.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.apps.api.routes.circles
"""campus.api.routes.circles

API routes for the circles resource.
"""
Expand Down
2 changes: 1 addition & 1 deletion campus/api/routes/emailotp.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.apps.api.routes.emailotp
"""campus.api.routes.emailotp

API routes for the emailotp resource.
"""
Expand Down
2 changes: 1 addition & 1 deletion campus/audit/helpers/audit_events.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion campus/audit/routes/apikeys.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.audit.routes.api_keys
"""campus.audit.routes.apikeys

API key management endpoints for the audit service.

Expand Down
13 changes: 11 additions & 2 deletions campus/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion campus/auth/oauth_proxy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.oauth.routes
"""campus.auth.oauth_proxy

This module contains the route definitions for Campus and third-party
OAuth2 authentication.
Expand Down
2 changes: 1 addition & 1 deletion campus/auth/oauth_proxy/discord/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.auth.routes.discord
"""campus.auth.oauth_proxy.discord

Routes for Discord OAuth2 Client Credentials flow.

Expand Down
2 changes: 1 addition & 1 deletion campus/auth/oauth_proxy/github/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.auth.routes.github
"""campus.auth.oauth_proxy.github

Proxy and callback routes for GitHub OAuth2 authentication.
"""
Expand Down
2 changes: 1 addition & 1 deletion campus/auth/oauth_proxy/google/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.auth.routes.google
"""campus.auth.oauth_proxy.google

Routes for Google OAuth2.

Expand Down
2 changes: 1 addition & 1 deletion campus/auth/resources/access.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.auth.access
"""campus.auth.resources.access

Defines access control utilities for Campus authentication and vault access.
"""
Expand Down
2 changes: 1 addition & 1 deletion campus/auth/resources/user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.auth.user
"""campus.auth.resources.user

Implements Campus API for user access.
"""
Expand Down
2 changes: 1 addition & 1 deletion campus/auth/resources/vault.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.auth.vault
"""campus.auth.resources.vault

Implements Campus API for vault access.
"""
Expand Down
2 changes: 1 addition & 1 deletion campus/common/utils/utc_time.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion campus/flask_campus/login_manager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""flask_campus.login_manager
"""campus.flask_campus.login_manager

OAuth Login Manager for Flask integration.
"""
Expand Down
2 changes: 1 addition & 1 deletion campus/flask_campus/parameter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.common.flask.validation
"""campus.flask_campus.parameter

This module provides utilities for validation of arguments against
parameters.
Expand Down
2 changes: 1 addition & 1 deletion campus/flask_campus/types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""campus.flask_campus
"""campus.flask_campus.types

Common utility functions for validation of flask requests and responses.
"""
Expand Down
7 changes: 7 additions & 0 deletions campus/flask_campus/utils.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion campus/logging_config.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
5 changes: 5 additions & 0 deletions campus/model/booking.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
"""campus.model.booking

Venue and venue-booking entity models.
"""

from dataclasses import dataclass, field

from campus.common import schema
Expand Down
144 changes: 76 additions & 68 deletions campus/yapper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,67 @@
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",
"YapperInterface",
"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

Expand All @@ -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
4 changes: 4 additions & 0 deletions docs/PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@ weekly → staging → main
auth, api → services, storage, model, common
storage → common
model → common
yapper → (nothing — self-contained)
```

**Key Constraints:**
- `auth` and `api` contain business logic in `.resources` submodules
- `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

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading