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.
- flight_alloc-0.0.1.dist-info/METADATA +11 -0
- flight_alloc-0.0.1.dist-info/RECORD +71 -0
- flight_alloc-0.0.1.dist-info/WHEEL +5 -0
- flight_alloc-0.0.1.dist-info/entry_points.txt +2 -0
- flight_alloc-0.0.1.dist-info/top_level.txt +1 -0
- src/__init__.py +0 -0
- src/allocator/__init__.py +6 -0
- src/allocator/caps.py +513 -0
- src/allocator/eligibility.py +302 -0
- src/allocator/greedy_fallback.py +106 -0
- src/allocator/invariants.py +124 -0
- src/allocator/p2f_priority.py +142 -0
- src/allocator/pair_validation.py +108 -0
- src/allocator/pairings.py +554 -0
- src/allocator/postpass_break.py +366 -0
- src/allocator/postpass_intl.py +483 -0
- src/allocator/postpass_p2f.py +723 -0
- src/allocator/postpass_rebalance.py +244 -0
- src/allocator/postsolve.py +549 -0
- src/allocator/recommender.py +348 -0
- src/allocator/windows.py +377 -0
- src/cli.py +41 -0
- src/config.py +168 -0
- src/greedy_fallback.py +102 -0
- src/io/__init__.py +0 -0
- src/io/export.py +270 -0
- src/io/export_xml.py +66 -0
- src/io/readers.py +1048 -0
- src/io/roster_library.py +89 -0
- src/plan.py +192 -0
- src/recommender_staffing.py +329 -0
- src/roster_store.py +159 -0
- src/schemas.py +1244 -0
- src/solver/__init__.py +0 -0
- src/solver/allocator_cpsat.py +1412 -0
- src/staged_overrides.py +468 -0
- src/state.py +494 -0
- src/step1_clean_flights.py +286 -0
- src/step2_extract_roster.py +316 -0
- src/step3_allocate_flights.py +1639 -0
- src/web/__init__.py +47 -0
- src/web/__main__.py +9 -0
- src/web/api/__init__.py +56 -0
- src/web/api/export.py +37 -0
- src/web/api/inputs.py +122 -0
- src/web/api/override_rows.py +138 -0
- src/web/api/pages.py +30 -0
- src/web/api/readbacks.py +72 -0
- src/web/api/recommender.py +72 -0
- src/web/api/runs.py +102 -0
- src/web/api/settings.py +201 -0
- src/web/api/zc.py +117 -0
- src/web/core/__init__.py +5 -0
- src/web/core/responses.py +91 -0
- src/web/core/router.py +167 -0
- src/web/core/static_files.py +85 -0
- src/web/overrides/__init__.py +66 -0
- src/web/overrides/airports.py +261 -0
- src/web/overrides/break_time.py +83 -0
- src/web/overrides/config_yaml.py +21 -0
- src/web/overrides/filters.py +187 -0
- src/web/overrides/rows.py +110 -0
- src/web/readback/__init__.py +67 -0
- src/web/readback/common.py +68 -0
- src/web/readback/dashboard.py +83 -0
- src/web/readback/planning.py +335 -0
- src/web/readback/session.py +158 -0
- src/web/readback/tables.py +163 -0
- src/web/runner.py +168 -0
- src/web/server.py +185 -0
- 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
|