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.
- mfup_fastapi/__init__.py +40 -0
- mfup_fastapi/__main__.py +13 -0
- mfup_fastapi/app.py +18 -0
- mfup_fastapi/config.py +78 -0
- mfup_fastapi/engine.py +1118 -0
- mfup_fastapi/py.typed +0 -0
- mfup_fastapi-0.2.0.dist-info/METADATA +92 -0
- mfup_fastapi-0.2.0.dist-info/RECORD +10 -0
- mfup_fastapi-0.2.0.dist-info/WHEEL +4 -0
- mfup_fastapi-0.2.0.dist-info/licenses/LICENSE +21 -0
mfup_fastapi/__init__.py
ADDED
|
@@ -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
|
+
]
|
mfup_fastapi/__main__.py
ADDED
|
@@ -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
|
+
)
|