|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +Scitrera Application Framework (SAF) is a lightweight Python application framework for services, CLIs, and desktop apps. It provides: |
| 8 | +- Environment/variables abstraction with layered sources and ergonomic access |
| 9 | +- Structured logging with JSON formatting option |
| 10 | +- Simple plugin/extension system supporting dependency injection and OSGi-style multi-extensions |
| 11 | +- Optional stateful working directory management |
| 12 | +- Background execution, multi-tenant configuration, and Pyroscope profiling plugins |
| 13 | + |
| 14 | +## Development Commands |
| 15 | + |
| 16 | +```bash |
| 17 | +# Install (editable) |
| 18 | +pip install -e . |
| 19 | + |
| 20 | +# Install test dependencies |
| 21 | +pip install pytest pytest-cov python-dotenv |
| 22 | + |
| 23 | +# Run full test suite |
| 24 | +pytest tests/ -v |
| 25 | + |
| 26 | +# Run a single test file |
| 27 | +pytest tests/test_api_variables.py -v |
| 28 | + |
| 29 | +# Run a single test |
| 30 | +pytest tests/test_api_variables.py::TestVariablesBasic::test_set_and_get -v |
| 31 | + |
| 32 | +# Run with coverage |
| 33 | +pytest tests/ --cov=scitrera_app_framework --cov-report=term-missing |
| 34 | + |
| 35 | +``` |
| 36 | + |
| 37 | +## Architecture |
| 38 | + |
| 39 | +### Core Initialization Flow |
| 40 | + |
| 41 | +1. `init_framework(app_name)` → creates/returns a `Variables` instance |
| 42 | +2. Logging is configured (JSON or %-format based on `LOGGING_FORMAT`) |
| 43 | +3. Stateful paths optionally set up under `STATEFUL_ROOT` |
| 44 | +4. Shutdown hooks registered (SIGTERM or atexit) |
| 45 | +5. Plugins registered and initialized |
| 46 | + |
| 47 | +Alternate init functions with different defaults: |
| 48 | +- `init_framework_desktop()` – uses `~/.config/$APP_NAME`, atexit hooks |
| 49 | +- `init_framework_test_harness()` – DEBUG logging, no stateful/shutdown |
| 50 | +- `init_framework_embedded()` – avoids overriding external logging config |
| 51 | + |
| 52 | +### Variables System (`api/variables.py`) |
| 53 | + |
| 54 | +The `Variables` class is the central configuration container. It searches sources in priority order: |
| 55 | +1. Process environment (`os.environ` with uppercase keys) |
| 56 | +2. Local settings (`v.set()`) |
| 57 | +3. Additional sources added via `v.add_source()` |
| 58 | +4. Fallback defaults |
| 59 | + |
| 60 | +Key methods: |
| 61 | +- `v.environ(key, default=, type_fn=)` – get with default/type registration |
| 62 | +- `v.set(key, value)` – set local value |
| 63 | +- `v.get_by_prefix(prefix)` – extract namespaced config as dict (useful for kwargs) |
| 64 | +- `v.import_from_env_by_prefix(prefix)` – import env vars matching prefix |
| 65 | + |
| 66 | +`EnvPlacement` enum controls where environment appears in search order (TOP, BOTTOM, BOTTOM2, IGNORED). |
| 67 | + |
| 68 | +### Plugin System (`api/plugins.py`, `core/plugins.py`) |
| 69 | + |
| 70 | +Plugins implement the `Plugin` abstract class: |
| 71 | +- `extension_point_name(v)` – the named slot this plugin fills |
| 72 | +- `is_enabled(v)` – for single-extension mode (dependency injection) |
| 73 | +- `is_multi_extension(v)` – for OSGi-style multiple implementations |
| 74 | +- `get_dependencies(v)` – list of extension points that must init first |
| 75 | +- `initialize(v, logger)` – returns the extension value |
| 76 | +- `shutdown(v, logger, value)` – cleanup |
| 77 | + |
| 78 | +Registration: |
| 79 | +```python |
| 80 | +register_plugin(MyPlugin, v, init=True) # register and initialize |
| 81 | +value = get_extension('ext-name', v) # single extension |
| 82 | +values = get_extensions('ext-name', v) # multi-extension (returns dict) |
| 83 | +``` |
| 84 | + |
| 85 | +Built-in plugins: |
| 86 | +- `EXT_BACKGROUND_EXEC` – thread pool via `get_background_exec()` |
| 87 | +- `EXT_PROGRESS_TRACKER` – progress tracking for UIs |
| 88 | + |
| 89 | +### Package Layout |
| 90 | + |
| 91 | +``` |
| 92 | +scitrera_app_framework/ |
| 93 | +├── api/ # Variables and Plugin base types (public API) |
| 94 | +├── core/ # Framework init, logging, plugin registry (internal) |
| 95 | +├── base_plugins/ # Built-in optional plugins (bg_exec, progress_tracker) |
| 96 | +├── ext_plugins/ # Optional extensions (pyroscope, multi-tenant) |
| 97 | +├── k8s/ # Kubernetes utilities (apply_yaml_object, start_pod, etc.) |
| 98 | +├── slaunch/ # Conda environment management and app launching utilities |
| 99 | +└── util/ # Parsing, imports, async helpers (no api/core deps) |
| 100 | +``` |
| 101 | + |
| 102 | +## Code Conventions |
| 103 | + |
| 104 | +- Internal framework variables use "epp" keys: `=|name|` (equal-pipe-pipe pattern) |
| 105 | +- Type functions for environment parsing: `ext_parse_bool`, `ext_parse_csv`, `ext_get_python` |
| 106 | +- Use lazy string formatting in logging: `logger.debug("item: %s", item)` not f-strings |
| 107 | +- The `v` parameter is conventionally the Variables instance; `None` uses the default singleton |
| 108 | + |
| 109 | +## Key Environment Variables |
| 110 | + |
| 111 | +Core: |
| 112 | +- `APP_NAME` – override computed app name |
| 113 | +- `LOGGING_LEVEL` – log level (default from init kwarg) |
| 114 | +- `LOGGING_FORMAT` – `'json'` for JSON logs, or a %-format string |
| 115 | + |
| 116 | +Stateful: |
| 117 | +- `STATEFUL_ROOT` – root directory for stateful data (default: `./scratch`) |
| 118 | +- `SAF_SETUP_STATEFUL` – enable/disable stateful features |
| 119 | +- `SAF_STATEFUL_CHDIR` – whether to chdir into stateful path |
| 120 | + |
| 121 | +Plugins: |
| 122 | +- `SAF_BASE_PLUGINS` – auto-register base plugins |
| 123 | +- `PYROSCOPE_ENABLED` – enable Pyroscope profiling |
| 124 | +- `SAF_MULTITENANT_ENABLED` – enable multi-tenant plugin |
0 commit comments