custmatch-server 0.1.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.
- custmatch_server/__init__.py +7 -0
- custmatch_server/__main__.py +92 -0
- custmatch_server/api.py +869 -0
- custmatch_server/auth.py +86 -0
- custmatch_server/db.py +279 -0
- custmatch_server/health.py +127 -0
- custmatch_server/loader.py +166 -0
- custmatch_server/matcher.py +834 -0
- custmatch_server/migrations/env.py +7 -0
- custmatch_server/migrations/script.py.mako +22 -0
- custmatch_server/migrations/versions/0001_initial.py +135 -0
- custmatch_server/migrations/versions/0002_overrides.py +43 -0
- custmatch_server/migrations/versions/0003_api_keys.py +31 -0
- custmatch_server/migrations/versions/0004_stewardship.py +70 -0
- custmatch_server/migrations/versions/0005_decisions.py +52 -0
- custmatch_server/scheduler.py +147 -0
- custmatch_server/schema.py +206 -0
- custmatch_server/static/dashboard.html +1046 -0
- custmatch_server/worker.py +160 -0
- custmatch_server-0.1.0.dist-info/METADATA +289 -0
- custmatch_server-0.1.0.dist-info/RECORD +26 -0
- custmatch_server-0.1.0.dist-info/WHEEL +5 -0
- custmatch_server-0.1.0.dist-info/entry_points.txt +2 -0
- custmatch_server-0.1.0.dist-info/licenses/LICENSE +202 -0
- custmatch_server-0.1.0.dist-info/licenses/NOTICE +24 -0
- custmatch_server-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""custmatch as a self-hosted service - one image, one database (server/README.md).
|
|
2
|
+
|
|
3
|
+
db the schema and the publish transaction (SQLite or Postgres through SQLAlchemy)
|
|
4
|
+
worker a batch run: custmatch run, then publish to the database
|
|
5
|
+
matcher real-time matching against the current run, one writer per database (a lease)
|
|
6
|
+
api lookups and POST /match (FastAPI)
|
|
7
|
+
"""
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""custmatch-server load CONFIG SOURCE FILE [--full] load one source's records into the database
|
|
2
|
+
custmatch-server batch CONFIG --labels LABELS a batch run over the loaded records, published
|
|
3
|
+
custmatch-server serve [--host 0.0.0.0] [--port 8000] the API
|
|
4
|
+
custmatch-server key create NAME --role ROLE an API key (shown once); roles read, match, steward, admin
|
|
5
|
+
custmatch-server key revoke NAME | key list
|
|
6
|
+
custmatch-server schedule CONFIG --labels LABELS --inbox DIR [--batch-at HH:MM ...] [--poll 60] [--once]
|
|
7
|
+
load the inbox, run batches on a schedule, alert (scheduler.py)
|
|
8
|
+
custmatch-server check the health checks; exit 0 ok, 1 warn, 2 fail (health.py)
|
|
9
|
+
|
|
10
|
+
A load is incremental (adds and changes records) unless --full, which also marks records missing
|
|
11
|
+
from the file as deleted. The database comes from CUSTMATCH_DB_URL (default sqlite:///custmatch.db;
|
|
12
|
+
for Postgres postgresql+psycopg://user:password@host/db); run files go under CUSTMATCH_DATA_DIR
|
|
13
|
+
(default ./data).
|
|
14
|
+
"""
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from . import db
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(argv=None):
|
|
25
|
+
ap = argparse.ArgumentParser(prog="custmatch-server")
|
|
26
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
27
|
+
ld = sub.add_parser("load")
|
|
28
|
+
ld.add_argument("config")
|
|
29
|
+
ld.add_argument("source")
|
|
30
|
+
ld.add_argument("file")
|
|
31
|
+
ld.add_argument("--full", action="store_true")
|
|
32
|
+
b = sub.add_parser("batch")
|
|
33
|
+
b.add_argument("config")
|
|
34
|
+
b.add_argument("--labels", required=True)
|
|
35
|
+
k = sub.add_parser("key")
|
|
36
|
+
k.add_argument("action", choices=["create", "revoke", "list"])
|
|
37
|
+
k.add_argument("name", nargs="?")
|
|
38
|
+
k.add_argument("--role", default="read")
|
|
39
|
+
sc = sub.add_parser("schedule")
|
|
40
|
+
sc.add_argument("config")
|
|
41
|
+
sc.add_argument("--labels", required=True)
|
|
42
|
+
sc.add_argument("--inbox", required=True)
|
|
43
|
+
sc.add_argument("--batch-at", action="append", default=[], help="UTC HH:MM, once a day; repeat for more")
|
|
44
|
+
sc.add_argument("--poll", type=int, default=60)
|
|
45
|
+
sc.add_argument("--once", action="store_true", help="load the inbox, run a batch, check, exit")
|
|
46
|
+
sub.add_parser("check")
|
|
47
|
+
s = sub.add_parser("serve")
|
|
48
|
+
s.add_argument("--host", default="127.0.0.1")
|
|
49
|
+
s.add_argument("--port", type=int, default=8000)
|
|
50
|
+
a = ap.parse_args(argv)
|
|
51
|
+
eng = db.engine_from_env()
|
|
52
|
+
if a.cmd == "load":
|
|
53
|
+
from .loader import load_file
|
|
54
|
+
cfg = json.loads(Path(a.config).read_text())
|
|
55
|
+
print(json.dumps(load_file(eng, cfg, a.source, a.file, mode="full" if a.full else "incremental")))
|
|
56
|
+
elif a.cmd == "key":
|
|
57
|
+
from . import auth
|
|
58
|
+
if a.action == "list":
|
|
59
|
+
for r in auth.list_keys(eng):
|
|
60
|
+
print(r["name"], r["role"], r["prefix"] + "...", "revoked" if r["revoked_at"] else "live")
|
|
61
|
+
elif not a.name:
|
|
62
|
+
ap.error("key create/revoke needs a NAME")
|
|
63
|
+
elif a.action == "create":
|
|
64
|
+
print(auth.create_key(eng, a.name, a.role))
|
|
65
|
+
else:
|
|
66
|
+
print(f"revoked {auth.revoke(eng, a.name)} key(s) of {a.name}")
|
|
67
|
+
elif a.cmd == "schedule":
|
|
68
|
+
from .scheduler import Scheduler
|
|
69
|
+
sch = Scheduler(eng, a.config, a.labels, a.inbox, os.environ.get("CUSTMATCH_DATA_DIR", "data"), a.batch_at)
|
|
70
|
+
if a.once:
|
|
71
|
+
return {"ok": 0, "warn": 1, "fail": 2}[sch.once()]
|
|
72
|
+
sch.loop(a.poll)
|
|
73
|
+
elif a.cmd == "check":
|
|
74
|
+
from . import health
|
|
75
|
+
results = health.checks(eng)
|
|
76
|
+
for r in results:
|
|
77
|
+
print(f"{r['status']:5} {r['check']:10} {r['detail']}")
|
|
78
|
+
return {"ok": 0, "warn": 1, "fail": 2}[health.worst(results)]
|
|
79
|
+
elif a.cmd == "batch":
|
|
80
|
+
from .worker import run_batch
|
|
81
|
+
run_batch(eng, a.config, a.labels, os.environ.get("CUSTMATCH_DATA_DIR", "data"))
|
|
82
|
+
else:
|
|
83
|
+
import uvicorn
|
|
84
|
+
from .api import create_app
|
|
85
|
+
if os.environ.get("CUSTMATCH_AUTH", "on").lower() == "off" and a.host not in ("127.0.0.1", "localhost", "::1"):
|
|
86
|
+
ap.error("CUSTMATCH_AUTH=off answers anyone who can reach the API: serve it on 127.0.0.1 only")
|
|
87
|
+
uvicorn.run(create_app(eng), host=a.host, port=a.port)
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
sys.exit(main())
|