{"id":35711259,"url":"https://github.com/serendip-ml/appinfra","last_synced_at":"2026-02-03T08:08:21.578Z","repository":{"id":331600137,"uuid":"1127445049","full_name":"serendip-ml/appinfra","owner":"serendip-ml","description":"Production-grade Python infrastructure framework for building reliable CLI tools and services","archived":false,"fork":false,"pushed_at":"2026-01-10T19:49:12.000Z","size":1132,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"develop","last_synced_at":"2026-01-11T02:51:11.376Z","etag":null,"topics":["cli","configuration","framework","infrastructure","logging","python"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/appinfra/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/serendip-ml.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-03T22:36:38.000Z","updated_at":"2026-01-10T19:49:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/serendip-ml/appinfra","commit_stats":null,"previous_names":["serendip-ml/appinfra"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/serendip-ml/appinfra","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serendip-ml%2Fappinfra","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serendip-ml%2Fappinfra/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serendip-ml%2Fappinfra/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serendip-ml%2Fappinfra/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/serendip-ml","download_url":"https://codeload.github.com/serendip-ml/appinfra/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serendip-ml%2Fappinfra/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28399861,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T14:36:09.778Z","status":"ssl_error","status_checked_at":"2026-01-13T14:35:19.697Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","configuration","framework","infrastructure","logging","python"],"created_at":"2026-01-06T04:12:23.106Z","updated_at":"2026-01-30T01:29:16.870Z","avatar_url":"https://github.com/serendip-ml.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# appinfra\n\n![Python](https://img.shields.io/badge/python-3.11+-blue.svg)\n![Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen.svg)\n![Type Hints](https://img.shields.io/badge/type%20hints-100%25-brightgreen.svg)\n[![Typed](https://img.shields.io/badge/typed-PEP%20561-brightgreen.svg)](https://peps.python.org/pep-0561/)\n[![Linting: Ruff](https://img.shields.io/badge/linting-ruff-brightgreen)](https://github.com/astral-sh/ruff)\n[![CI](https://github.com/serendip-ml/appinfra/actions/workflows/test-docker.yml/badge.svg)](https://github.com/serendip-ml/appinfra/actions/workflows/test-docker.yml)\n[![PyPI](https://img.shields.io/pypi/v/appinfra)](https://pypi.org/project/appinfra/)\n![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)\n\nProduction-grade Python infrastructure framework for building reliable CLI tools and services.\n\n## Scope\n\n**Best for:** Production CLI tools, background services, systems-level Python applications.\n\n**Not for:** Web APIs (use FastAPI), async-heavy applications, ORMs.\n\nSee [docs/README.md](appinfra/docs/README.md) for full scope and philosophy.\n\n## Features\n\n- **Logging** - Structured logging with custom levels, rotation, JSON output, and database handlers\n- **Database** - PostgreSQL interface with connection pooling and query monitoring\n- **App Framework** - Fluent builder API for CLI tools with lifecycle management\n- **Configuration** - YAML config with environment variable overrides and path resolution\n- **Time Utilities** - Scheduling, periodic execution, and duration formatting\n\n## Requirements\n\n- Python 3.11+\n- PostgreSQL 16 (optional, for database features)\n\n## Installation\n\n```bash\npip install appinfra\n```\n\nOptional features:\n\n```bash\npip install appinfra[ui]         # Rich console, interactive prompts\npip install appinfra[fastapi]    # FastAPI integration\npip install appinfra[validation] # Pydantic config validation\npip install appinfra[hotreload]  # Config file watching\n```\n\n## Documentation\n\nFull documentation is available in [docs/README.md](appinfra/docs/README.md), or via CLI:\n\n```bash\nappinfra docs           # Overview\nappinfra docs list      # List all guides and examples\nappinfra docs show \u003ctopic\u003e  # Read a specific guide\n```\n\n## Highlights\n\n### App Framework\n\n**AppBuilder for CLI tools** - Build production CLI applications with lifecycle management, config,\nlogging, and tools. Focused configurers provide clean separation of concerns. Config files are\nresolved from `--etc-dir` (default: `./etc`):\n\n```python\nfrom appinfra.app import AppBuilder\n\napp = (\n    AppBuilder(\"myapp\")\n    .with_description(\"Data processing tool\")\n    .with_config_file(\"config.yaml\")              # Resolved from --etc-dir\n    .logging.with_level(\"info\").with_location(1).done()\n    .tools.with_tool(ProcessorTool()).with_main(MainTool()).done()\n    .advanced.with_hook(\"startup\", init_database).done()\n    .build()\n)\n\napp.run()\n```\n\n**Fluent builder APIs** - All components use chainable builder patterns for clean, readable\nconfiguration. No more scattered setup code or complex constructor arguments:\n\n```python\nfrom appinfra.log import LoggingBuilder\n\nlogger = (\n    LoggingBuilder(\"my_app\")\n    .with_level(\"info\")\n    .with_format(\"%(asctime)s [%(levelname)s] %(message)s\")\n    .console_handler(colors=True)\n    .file_handler(\"logs/app.log\", rotate_mb=10)\n    .build()\n)\n```\n\n**Decorator-based CLI tools** - Build command-line tools with minimal boilerplate. Tools\nautomatically get logging, config access, and argument parsing:\n\n```python\nfrom appinfra.app import AppBuilder\n\napp = AppBuilder(\"mytool\").build()\n\n@app.tool(name=\"sync\", help=\"Synchronize data\")\n@app.argument(\"--force\", action=\"store_true\", help=\"Force sync\")\n@app.argument(\"--limit\", type=int, default=100)\ndef sync_tool(self):\n    self.lg.info(f\"Syncing {self.args.limit} items\")\n    if self.args.force:\n        self.lg.warning(\"Force mode enabled\")\n    return 0\n```\n\n**Nested subcommands** - Organize complex CLIs with hierarchical command structures using the\n`@subtool` decorator:\n\n```python\napp = AppBuilder(\"myapp\").build()\n\n@app.tool(name=\"db\", help=\"Database operations\")\ndef db_tool(self):\n    return self.run_subtool()\n\n@db_tool.subtool(name=\"migrate\", help=\"Run migrations\")\n@app.argument(\"--step\", type=int, default=1)\ndef db_migrate(self):\n    self.lg.info(f\"Migrating {self.args.step} steps...\")\n\n@db_tool.subtool(name=\"status\")\ndef db_status(self):\n    self.lg.info(\"Database is healthy\")\n\n# Usage: myapp db migrate --step 3\n#        myapp db status\n```\n\n**Multi-source version tracking** - Automatically detect version and git commit from PEP 610\nmetadata, build-time info, or git runtime. Integrates with AppBuilder for --version flag and\nstartup logging:\n\n```python\napp = (\n    AppBuilder(\"myapp\")\n    .version\n        .with_semver(\"1.0.0\")\n        .with_build_info()              # App's own commit from _build_info.py\n        .with_package(\"appinfra\")       # Track framework version\n        .done()\n    .build()\n)\n# --version shows: myapp 1.0.0 (abc123f) + tracked packages\n# Startup logs commit hash, warns if repo has uncommitted changes\n```\n\n### Configuration\n\n**YAML includes with security** - Build modular configurations with file includes, environment\nvariable validation, and automatic path resolution. Includes are protected against path traversal\nand circular dependencies:\n\n```yaml\n# config.yaml\n!include \"./base.yaml\"                    # Document-level merge\n\ndatabase:\n  primary: !include \"./db/primary.yaml\"   # Nested includes\n  credentials:\n    password: !secret ${DB_PASSWORD}      # Validated env var reference\n\npaths:\n  models: !path ../models                 # Resolved relative to this file\n  cache: !path ~/.cache/myapp             # Expands ~\n```\n\n**DotDict config access** - Access nested configuration with attribute syntax or dot-notation paths.\nAutomatic conversion of nested dicts, with safe traversal methods:\n\n```python\nfrom appinfra.dot_dict import DotDict\n\nconfig = DotDict({\n    \"database\": {\"host\": \"localhost\", \"port\": 5432},\n    \"features\": {\"beta\": True}\n})\n\n# Attribute-style access\nprint(config.database.host)               # \"localhost\"\nprint(config.features.beta)               # True\n\n# Dot-notation path queries\nif config.has(\"database.ssl.enabled\"):\n    setup_ssl(config.get(\"database.ssl.cert\"))\n```\n\n**Hot-reload configuration** - Change log levels, feature flags, or any config value without\nrestarting your application. Uses content-based change detection to avoid spurious reloads:\n\n```python\nfrom appinfra.config import ConfigWatcher\n\ndef on_config_change(new_config):\n    logger.info(\"Config updated, applying changes...\")\n    apply_feature_flags(new_config.features)\n\nwatcher = ConfigWatcher(lg=logger, etc_dir=\"./etc\")\nwatcher.configure(\"config.yaml\", debounce_ms=500)\nwatcher.add_section_callback(\"features\", on_config_change)\nwatcher.start()\n```\n\n### Logging \u0026 Security\n\n**Topic-based log levels** - Control logging granularity with glob patterns. Set debug logging for\ndatabase queries while keeping network calls at warning level, all without touching application\ncode:\n\n```python\nfrom appinfra.log import LogLevelManager\n\nmanager = LogLevelManager.get_instance()\nmanager.add_rule(\"/app/db/*\", \"debug\")      # All database loggers\nmanager.add_rule(\"/app/db/queries\", \"trace\") # Even more detail for queries\nmanager.add_rule(\"/app/net/**\", \"warning\")   # Network and all children\nmanager.add_rule(\"/app/cache\", \"error\")      # Only errors from cache\n```\n\n**Automatic secret masking** - Protect sensitive data in logs with pattern-based detection. Covers\n20+ secret formats including AWS keys, GitHub tokens, JWTs, and database URLs:\n\n```python\nfrom appinfra.security import SecretMasker, SecretMaskingFilter\n\nmasker = SecretMasker()\nmasker.add_known_secret(os.environ[\"API_KEY\"])  # Track known secrets\n\n# Patterns auto-detect common formats\ntext = masker.mask(\"token=ghp_abc123secret\")    # \"token=[MASKED]\"\ntext = masker.mask(\"aws_secret=AKIA...\")        # \"aws_secret=[MASKED]\"\n\n# Integrate with logging\nhandler.addFilter(SecretMaskingFilter(masker))\n```\n\n**Lightweight observability hooks** - Event-based callbacks without heavy frameworks. Register\nhandlers for specific events or globally, with automatic timing in context:\n\n```python\nfrom appinfra.observability import ObservabilityHooks, HookEvent, HookContext\n\nhooks = ObservabilityHooks()\n\n@hooks.on(HookEvent.QUERY_START)\ndef on_query(ctx: HookContext):\n    logger.debug(f\"Query: {ctx.data.get('sql')}\")\n\n@hooks.on(HookEvent.QUERY_END)\ndef on_complete(ctx: HookContext):\n    logger.info(f\"Completed in {ctx.duration:.3f}s\")\n\n# Trigger events with arbitrary data\nhooks.trigger(HookEvent.QUERY_START, sql=\"SELECT * FROM users\")\n```\n\n### Time \u0026 Scheduling\n\n**Dual-mode ticker** - Run periodic tasks with scheduled intervals or continuous execution. Context\nmanager handles signals for graceful shutdown:\n\n```python\nfrom appinfra.time import Ticker\n\n# Scheduled mode: run every 30 seconds\nwith Ticker(logger, secs=30) as ticker:\n    for tick_count in ticker:           # Stops on SIGTERM/SIGINT\n        run_health_check()\n        if tick_count \u003e= 100:\n            break\n\n# Continuous mode: run as fast as possible\nfor tick in Ticker(logger):              # No secs = continuous\n    process_queue_item()\n```\n\n**Human-readable durations** - Format seconds to readable strings and parse them back. Supports\nmicroseconds to days, with precise mode for sub-millisecond accuracy:\n\n```python\nfrom appinfra.time import delta_str, delta_to_secs\n\n# Formatting\ndelta_str(3661.5)                # \"1h1m1s\"\ndelta_str(0.000042)              # \"42μs\"\ndelta_str(90061)                 # \"1d1h1m1s\"\n\n# Parsing\ndelta_to_secs(\"2h30m\")           # 9000.0\ndelta_to_secs(\"1d12h\")           # 129600.0\ndelta_to_secs(\"500ms\")           # 0.5\n```\n\n**Time-based task scheduler** - Execute tasks at specific times with daily, weekly, monthly, or\nhourly periods. Generator-based iteration with signal handling for graceful shutdown:\n\n```python\nfrom appinfra.time import Sched, Period\n\n# Daily at 14:30\nsched = Sched(logger, Period.DAILY, \"14:30\")\n\n# Weekly on Monday at 09:00\nsched = Sched(logger, Period.WEEKLY, \"09:00\", weekday=0)\n\nfor timestamp in sched.run():       # Yields after each scheduled time\n    generate_report()\n```\n\n**ETA progress tracking** - Accurate time-to-completion estimates using EWMA-smoothed processing\nrates. Handles variable update intervals without spike errors:\n\n```python\nfrom appinfra.time import ETA, delta_str\n\neta = ETA(total=1000)\nfor i, item in enumerate(items):\n    process(item)\n    eta.update(i + 1)\n    remaining = eta.remaining_secs()\n    print(f\"{eta.percent():.1f}% - {delta_str(remaining)} remaining\")\n```\n\n**Business day iteration** - Memory-efficient date range processing with weekend filtering. Iterates\nfrom start date to today without materializing the full range:\n\n```python\nfrom appinfra.time import iter_dates\nimport datetime\n\nstart = datetime.date(2025, 12, 1)\nfor date in iter_dates(start, skip_weekends=True):\n    process_business_day(date)              # Mon-Fri only, up to today\n```\n\n### CLI \u0026 UI\n\n**Testable CLI output** - Write testable CLI tools without mocking stdout. Swap output\nimplementations for production, testing, or silent operation:\n\n```python\nfrom appinfra.cli.output import ConsoleOutput, BufferedOutput, NullOutput\n\ndef run_command(output=None):\n    output = output or ConsoleOutput()\n    output.write(\"Processing...\")\n    output.write(\"Done!\")\n\n# In tests: capture output\nbuf = BufferedOutput()\nrun_command(output=buf)\nassert \"Done!\" in buf.text\nassert buf.lines == [\"Processing...\", \"Done!\"]\n```\n\n**Interactive CLI prompts** - Smart prompts that work in TTY, non-interactive, and CI environments.\nAuto-detects available libraries with graceful fallbacks:\n\n```python\nfrom appinfra.ui import confirm, select, text\n\nenv = select(\"Environment:\", [\"dev\", \"staging\", \"prod\"])\nname = text(\"Project name:\", validate=lambda x: len(x) \u003e 0)\n\nif confirm(f\"Deploy {name} to {env}?\"):\n    deploy()\n```\n\n**Progress with logging coordination** - Rich spinner or progress bar that pauses for log output.\nFalls back to plain logging on non-TTY:\n\n```python\nfrom appinfra.ui import ProgressLogger\n\nwith ProgressLogger(logger, \"Processing...\", total=100) as pl:\n    for item in items:\n        result = process(item)\n        pl.log(f\"Processed {item.name}\")     # Pauses spinner, logs, resumes\n        pl.update(advance=1)\n```\n\n### Database\n\n**Database auto-reconnection** - Automatic retry with exponential backoff on transient failures.\nConfigured via YAML, transparent to application code:\n\n```yaml\n# etc/config.yaml\ndatabase:\n  url: postgresql://...\n  auto_reconnect: true\n  max_retries: 3        # Attempts before raising\n  retry_delay: 0.5      # Initial delay, doubles each retry\n```\n\n**Read-only database mode** - Transaction-level enforcement preventing accidental writes. Validates\nconfiguration to catch conflicts early:\n\n```python\npg = PG(config, readonly=True)\nwith pg.session() as session:\n    # SELECT queries work normally\n    # INSERT/UPDATE/DELETE raise errors at transaction level\n```\n\n### Server\n\n**FastAPI subprocess isolation** - Run FastAPI in a subprocess with queue-based IPC. Main process\nstays responsive while workers handle requests, with automatic restart on failure:\n\n```python\nfrom appinfra.app.fastapi import FastAPIBuilder\n\nserver = (\n    FastAPIBuilder(\"api\")\n    .with_config(config)\n    .with_port(8000)\n    .with_subprocess_mode(\n        request_queue=request_q,\n        response_queue=response_q,\n        auto_restart=True\n    )\n    .build()\n)\n\nserver.start()  # Non-blocking, runs in subprocess\n```\n\n## Completeness\n\nBuilt for production with comprehensive validation:\n\n- **4,000+ tests** across unit, integration, e2e, security, and performance categories\n- **95% code coverage** on 11,000+ statements\n- **100% type hints** verified by mypy strict mode\n- **Security tests** for YAML injection, path traversal, ReDoS, and secret exposure\n\n## Contributing\n\nSee the [Contributing Guide](appinfra/docs/guides/contributing.md) for development setup and\nguidelines.\n\n## Links\n\n- [Changelog](CHANGELOG.md)\n- [Security Policy](appinfra/docs/SECURITY.md)\n- [API Stability](appinfra/docs/guides/api-stability.md)\n\n## License\n\nApache License 2.0 - see [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fserendip-ml%2Fappinfra","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fserendip-ml%2Fappinfra","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fserendip-ml%2Fappinfra/lists"}