flight-alloc 0.0.1__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.
Files changed (71) hide show
  1. flight_alloc-0.0.1.dist-info/METADATA +11 -0
  2. flight_alloc-0.0.1.dist-info/RECORD +71 -0
  3. flight_alloc-0.0.1.dist-info/WHEEL +5 -0
  4. flight_alloc-0.0.1.dist-info/entry_points.txt +2 -0
  5. flight_alloc-0.0.1.dist-info/top_level.txt +1 -0
  6. src/__init__.py +0 -0
  7. src/allocator/__init__.py +6 -0
  8. src/allocator/caps.py +513 -0
  9. src/allocator/eligibility.py +302 -0
  10. src/allocator/greedy_fallback.py +106 -0
  11. src/allocator/invariants.py +124 -0
  12. src/allocator/p2f_priority.py +142 -0
  13. src/allocator/pair_validation.py +108 -0
  14. src/allocator/pairings.py +554 -0
  15. src/allocator/postpass_break.py +366 -0
  16. src/allocator/postpass_intl.py +483 -0
  17. src/allocator/postpass_p2f.py +723 -0
  18. src/allocator/postpass_rebalance.py +244 -0
  19. src/allocator/postsolve.py +549 -0
  20. src/allocator/recommender.py +348 -0
  21. src/allocator/windows.py +377 -0
  22. src/cli.py +41 -0
  23. src/config.py +168 -0
  24. src/greedy_fallback.py +102 -0
  25. src/io/__init__.py +0 -0
  26. src/io/export.py +270 -0
  27. src/io/export_xml.py +66 -0
  28. src/io/readers.py +1048 -0
  29. src/io/roster_library.py +89 -0
  30. src/plan.py +192 -0
  31. src/recommender_staffing.py +329 -0
  32. src/roster_store.py +159 -0
  33. src/schemas.py +1244 -0
  34. src/solver/__init__.py +0 -0
  35. src/solver/allocator_cpsat.py +1412 -0
  36. src/staged_overrides.py +468 -0
  37. src/state.py +494 -0
  38. src/step1_clean_flights.py +286 -0
  39. src/step2_extract_roster.py +316 -0
  40. src/step3_allocate_flights.py +1639 -0
  41. src/web/__init__.py +47 -0
  42. src/web/__main__.py +9 -0
  43. src/web/api/__init__.py +56 -0
  44. src/web/api/export.py +37 -0
  45. src/web/api/inputs.py +122 -0
  46. src/web/api/override_rows.py +138 -0
  47. src/web/api/pages.py +30 -0
  48. src/web/api/readbacks.py +72 -0
  49. src/web/api/recommender.py +72 -0
  50. src/web/api/runs.py +102 -0
  51. src/web/api/settings.py +201 -0
  52. src/web/api/zc.py +117 -0
  53. src/web/core/__init__.py +5 -0
  54. src/web/core/responses.py +91 -0
  55. src/web/core/router.py +167 -0
  56. src/web/core/static_files.py +85 -0
  57. src/web/overrides/__init__.py +66 -0
  58. src/web/overrides/airports.py +261 -0
  59. src/web/overrides/break_time.py +83 -0
  60. src/web/overrides/config_yaml.py +21 -0
  61. src/web/overrides/filters.py +187 -0
  62. src/web/overrides/rows.py +110 -0
  63. src/web/readback/__init__.py +67 -0
  64. src/web/readback/common.py +68 -0
  65. src/web/readback/dashboard.py +83 -0
  66. src/web/readback/planning.py +335 -0
  67. src/web/readback/session.py +158 -0
  68. src/web/readback/tables.py +163 -0
  69. src/web/runner.py +168 -0
  70. src/web/server.py +185 -0
  71. src/zc_store.py +221 -0
src/roster_store.py ADDED
@@ -0,0 +1,159 @@
1
+ """On-disk copy of the uploaded rosters, so they survive a restart.
2
+
3
+ The two roster files (staff, AM/ZC) are published once per period —
4
+ roughly monthly, sometimes a little longer or shorter — and the operator
5
+ should not have to re-upload them every time the server is started. Each
6
+ upload is therefore also written to ``data/rosters/`` and reloaded when
7
+ the server boots.
8
+
9
+ Layout::
10
+
11
+ data/rosters/
12
+ index.json one entry per stored roster
13
+ staff_roster_<stamp>_<id>.xlsx the file exactly as uploaded
14
+ am_roster_<stamp>_<id>.xlsx
15
+
16
+ ``index.json`` keeps what the UI needs without re-parsing the workbooks:
17
+ the original filename, the upload time and the list of dates the roster
18
+ has columns for. Which roster answers a given allocation date is decided
19
+ from that date list (see ``AppState.roster_files_for``).
20
+
21
+ Persistence is best-effort: if the folder is read-only the app keeps
22
+ working from memory and logs a warning. Override the location with the
23
+ ``FLIGHT_ALLOC_DATA_DIR`` environment variable.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import logging
30
+ import os
31
+ import tempfile
32
+ from dataclasses import dataclass
33
+ from datetime import date as date_t
34
+ from datetime import datetime
35
+ from pathlib import Path
36
+
37
+ logger = logging.getLogger("src.web")
38
+
39
+ _REPO_ROOT = Path(__file__).resolve().parents[1]
40
+
41
+
42
+ def data_dir() -> Path:
43
+ override = os.environ.get("FLIGHT_ALLOC_DATA_DIR")
44
+ return Path(override) if override else _REPO_ROOT / "data"
45
+
46
+
47
+ def _roster_dir() -> Path:
48
+ return data_dir() / "rosters"
49
+
50
+
51
+ @dataclass
52
+ class StoredRoster:
53
+ """One roster as read back from disk."""
54
+
55
+ id: str
56
+ kind: str
57
+ filename: str
58
+ uploaded_at: datetime
59
+ dates: list[date_t]
60
+ data: bytes
61
+
62
+
63
+ def _index_path() -> Path:
64
+ return _roster_dir() / "index.json"
65
+
66
+
67
+ def _read_index() -> list[dict]:
68
+ path = _index_path()
69
+ if not path.exists():
70
+ return []
71
+ try:
72
+ raw = json.loads(path.read_text(encoding="utf-8"))
73
+ except (OSError, json.JSONDecodeError) as exc:
74
+ logger.warning("[rosters] cannot read %s: %s", path, exc)
75
+ return []
76
+ return raw if isinstance(raw, list) else []
77
+
78
+
79
+ def _write_index(entries: list[dict]) -> None:
80
+ """Atomic replace, so a crash mid-write can't leave a torn index."""
81
+ root = _roster_dir()
82
+ root.mkdir(parents=True, exist_ok=True)
83
+ fd, tmp = tempfile.mkstemp(dir=root, suffix=".tmp")
84
+ try:
85
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
86
+ json.dump(entries, fh, indent=2)
87
+ os.replace(tmp, _index_path())
88
+ except OSError:
89
+ try:
90
+ os.unlink(tmp)
91
+ except OSError:
92
+ pass
93
+ raise
94
+
95
+
96
+ def _blob_name(entry: dict) -> str:
97
+ return entry["stored_as"]
98
+
99
+
100
+ def save(
101
+ *, file_id: str, kind: str, filename: str, uploaded_at: datetime,
102
+ dates: list[date_t], data: bytes,
103
+ ) -> None:
104
+ """Write the workbook and add (or replace) its index entry."""
105
+ try:
106
+ root = _roster_dir()
107
+ root.mkdir(parents=True, exist_ok=True)
108
+ stored_as = f"{kind}_{file_id}.xlsx"
109
+ (root / stored_as).write_bytes(data)
110
+ entries = [e for e in _read_index() if e.get("id") != file_id]
111
+ entries.append({
112
+ "id": file_id,
113
+ "kind": kind,
114
+ "filename": filename,
115
+ "uploaded_at": uploaded_at.isoformat(timespec="seconds"),
116
+ "dates": [d.isoformat() for d in sorted(dates)],
117
+ "stored_as": stored_as,
118
+ })
119
+ _write_index(entries)
120
+ except OSError as exc:
121
+ logger.warning(
122
+ "[rosters] could not persist %s (%s) — it stays in memory only",
123
+ filename, exc,
124
+ )
125
+
126
+
127
+ def delete(file_id: str) -> None:
128
+ try:
129
+ entries = _read_index()
130
+ keep = [e for e in entries if e.get("id") != file_id]
131
+ for e in entries:
132
+ if e.get("id") == file_id:
133
+ try:
134
+ (_roster_dir() / _blob_name(e)).unlink()
135
+ except (OSError, KeyError):
136
+ pass
137
+ if len(keep) != len(entries):
138
+ _write_index(keep)
139
+ except OSError as exc:
140
+ logger.warning("[rosters] could not delete %s from disk: %s", file_id, exc)
141
+
142
+
143
+ def load_all() -> list[StoredRoster]:
144
+ """Every roster on disk, skipping entries whose file has gone."""
145
+ out: list[StoredRoster] = []
146
+ for e in _read_index():
147
+ try:
148
+ blob = (_roster_dir() / _blob_name(e)).read_bytes()
149
+ out.append(StoredRoster(
150
+ id=str(e["id"]),
151
+ kind=str(e["kind"]),
152
+ filename=str(e["filename"]),
153
+ uploaded_at=datetime.fromisoformat(e["uploaded_at"]),
154
+ dates=[date_t.fromisoformat(d) for d in e.get("dates", [])],
155
+ data=blob,
156
+ ))
157
+ except (OSError, KeyError, ValueError) as exc:
158
+ logger.warning("[rosters] skipping unreadable entry %r: %s", e, exc)
159
+ return out