mfup-fastapi 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ """mfup-fastapi — FastAPI integration for the MFUP/2 upload engine.
2
+
3
+ from mfup_fastapi import MfupConfig, MfupEngine, create_app
4
+ """
5
+
6
+ from .config import MfupConfig
7
+ from .engine import (
8
+ MapFileHookError,
9
+ MfupEngine,
10
+ NotCommitted,
11
+ PublishError,
12
+ SessionNotFound,
13
+ TargetEscapes,
14
+ reconcile_orphans,
15
+ )
16
+
17
+
18
+ def create_app(config: "MfupConfig | None" = None):
19
+ """Lazy re-export: builds the standalone app without importing
20
+ mfup_fastapi.app (which reads the environment at import time)."""
21
+ from fastapi import FastAPI
22
+
23
+ engine = MfupEngine(config or MfupConfig.from_env())
24
+ app = FastAPI(title="MFUP/2 Server", lifespan=engine.lifespan)
25
+ app.include_router(engine.router)
26
+ app.state.mfup_engine = engine
27
+ return app
28
+
29
+
30
+ __all__ = [
31
+ "create_app",
32
+ "MapFileHookError",
33
+ "MfupConfig",
34
+ "MfupEngine",
35
+ "NotCommitted",
36
+ "PublishError",
37
+ "reconcile_orphans",
38
+ "SessionNotFound",
39
+ "TargetEscapes",
40
+ ]
@@ -0,0 +1,13 @@
1
+ """Run the standalone MFUP/2 server: python -m mfup_fastapi"""
2
+
3
+ import os
4
+
5
+ import uvicorn
6
+
7
+ if __name__ == "__main__":
8
+ uvicorn.run(
9
+ "mfup_fastapi.app:app",
10
+ host=os.environ.get("MFUP_HOST", "0.0.0.0"),
11
+ port=int(os.environ.get("MFUP_PORT", "8070")),
12
+ log_level="info",
13
+ )
mfup_fastapi/app.py ADDED
@@ -0,0 +1,18 @@
1
+ """Standalone MFUP/2 server — config from environment.
2
+
3
+ uvicorn mfup_fastapi.app:app
4
+
5
+ This module is the ONLY place that builds an app at import time
6
+ (``app = create_app()`` for uvicorn's module:attr convention). Library
7
+ consumers use MfupEngine/MfupConfig directly and never import this module.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+
14
+ from . import create_app
15
+
16
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
17
+
18
+ app = create_app()
mfup_fastapi/config.py ADDED
@@ -0,0 +1,78 @@
1
+ """MfupConfig — every knob of the FastAPI integration in one dataclass.
2
+
3
+ Two construction paths:
4
+ - ``MfupConfig(...)`` — library embedding, explicit values,
5
+ hooks as CALLABLES;
6
+ - ``MfupConfig.from_env()`` — standalone/container deployment, values
7
+ from MFUP_* env vars, hooks as dotted
8
+ paths ("pkg.module:callable").
9
+
10
+ No environment variable is read at import time anywhere in this package —
11
+ only from_env() touches os.environ.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import Any, Callable, Mapping, Optional, Union
20
+
21
+ #: A hook is either the callable itself or a dotted import path.
22
+ HookRef = Union[str, Callable[..., Any]]
23
+
24
+
25
+ @dataclass
26
+ class MfupConfig:
27
+ #: Global base directory for staging + publish (per-session base_dir from
28
+ #: the authorize hook overrides it per session).
29
+ base_dir: Path = Path("/tmp/mfup-uploads")
30
+ redis_url: str = "redis://redis:6379/0"
31
+ session_resume_ttl: int = 3600
32
+ leg_idle_timeout: int = 60
33
+ max_chunk_bytes: int = 262144
34
+ max_open_files: int = 1
35
+ max_pending_files: int = 64
36
+ sweep_interval: int = 300
37
+ staging_prefix: str = ".incoming"
38
+ #: Run the filesystem-orphan reconciliation every Nth sweep.
39
+ reconcile_every: int = 4
40
+ #: Minimum age before a staging dir may be reconciled as an orphan.
41
+ orphan_grace_seconds: int = 600
42
+ #: Buffered-body threshold for atomic batch POSTs.
43
+ max_buffered_body: int = 16 * 1024 * 1024
44
+ #: Cap on the JSON size of HELLO.meta.
45
+ max_meta_bytes: int = 16384
46
+ #: Bearer token for admin/debug routes. Empty → routes disabled.
47
+ admin_token: str = ""
48
+ #: Consumer hooks — callables or dotted paths (see mfup_core.hooks).
49
+ authorize: Optional[HookRef] = None
50
+ map_file: Optional[HookRef] = None
51
+ on_committed: Optional[HookRef] = None
52
+
53
+ @classmethod
54
+ def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "MfupConfig":
55
+ e: Mapping[str, str] = os.environ if env is None else env
56
+
57
+ def _int(name: str, default: int) -> int:
58
+ return int(e.get(name, str(default)))
59
+
60
+ return cls(
61
+ base_dir=Path(e.get("MFUP_BASE_DIR", "/tmp/mfup-uploads")),
62
+ redis_url=e.get("REDIS_URL", "redis://redis:6379/0"),
63
+ session_resume_ttl=_int("MFUP_SESSION_RESUME_TTL", 3600),
64
+ leg_idle_timeout=_int("MFUP_LEG_IDLE_TIMEOUT", 60),
65
+ max_chunk_bytes=_int("MFUP_MAX_CHUNK_BYTES", 262144),
66
+ max_open_files=_int("MFUP_MAX_OPEN_FILES", 1),
67
+ max_pending_files=_int("MFUP_MAX_PENDING_FILES", 64),
68
+ sweep_interval=_int("MFUP_SWEEP_INTERVAL", 300),
69
+ staging_prefix=e.get("MFUP_STAGING_PREFIX", ".incoming"),
70
+ reconcile_every=_int("MFUP_RECONCILE_EVERY", 4),
71
+ orphan_grace_seconds=_int("MFUP_ORPHAN_GRACE", 600),
72
+ max_buffered_body=_int("MFUP_MAX_BUFFERED_BODY", 16 * 1024 * 1024),
73
+ max_meta_bytes=_int("MFUP_MAX_META_BYTES", 16384),
74
+ admin_token=e.get("MFUP_ADMIN_TOKEN", ""),
75
+ authorize=e.get("MFUP_AUTHORIZE") or None,
76
+ map_file=e.get("MFUP_MAP_FILE") or None,
77
+ on_committed=e.get("MFUP_ON_COMMITTED") or None,
78
+ )