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.
@@ -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())