duplix 0.1.0__tar.gz
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.
- duplix-0.1.0/.gitignore +8 -0
- duplix-0.1.0/LICENSE +21 -0
- duplix-0.1.0/PKG-INFO +61 -0
- duplix-0.1.0/README.md +40 -0
- duplix-0.1.0/pyproject.toml +42 -0
- duplix-0.1.0/src/duplix/__init__.py +7 -0
- duplix-0.1.0/src/duplix/__main__.py +62 -0
- duplix-0.1.0/src/duplix/_template/.gitignore +3 -0
- duplix-0.1.0/src/duplix/_template/README.md +154 -0
- duplix-0.1.0/src/duplix/_template/configs/config.yml +256 -0
- duplix-0.1.0/src/duplix/_template/configs/shift_limits.json +28 -0
- duplix-0.1.0/src/duplix/_template/pyproject.toml +22 -0
- duplix-0.1.0/src/duplix/_template/requirements.txt +17 -0
- duplix-0.1.0/src/duplix/_template/src/__init__.py +0 -0
- duplix-0.1.0/src/duplix/_template/src/allocator/__init__.py +6 -0
- duplix-0.1.0/src/duplix/_template/src/allocator/caps.py +418 -0
- duplix-0.1.0/src/duplix/_template/src/allocator/eligibility.py +358 -0
- duplix-0.1.0/src/duplix/_template/src/allocator/pair_validation.py +108 -0
- duplix-0.1.0/src/duplix/_template/src/allocator/pairings.py +554 -0
- duplix-0.1.0/src/duplix/_template/src/allocator/postpass_intl.py +502 -0
- duplix-0.1.0/src/duplix/_template/src/allocator/postpass_p2f.py +731 -0
- duplix-0.1.0/src/duplix/_template/src/allocator/postsolve.py +589 -0
- duplix-0.1.0/src/duplix/_template/src/allocator/recommender.py +362 -0
- duplix-0.1.0/src/duplix/_template/src/allocator/windows.py +353 -0
- duplix-0.1.0/src/duplix/_template/src/cli.py +41 -0
- duplix-0.1.0/src/duplix/_template/src/config.py +147 -0
- duplix-0.1.0/src/duplix/_template/src/io/__init__.py +0 -0
- duplix-0.1.0/src/duplix/_template/src/io/export.py +272 -0
- duplix-0.1.0/src/duplix/_template/src/io/readers.py +884 -0
- duplix-0.1.0/src/duplix/_template/src/plan.py +200 -0
- duplix-0.1.0/src/duplix/_template/src/recommender_staffing.py +329 -0
- duplix-0.1.0/src/duplix/_template/src/schemas.py +1243 -0
- duplix-0.1.0/src/duplix/_template/src/solver/__init__.py +0 -0
- duplix-0.1.0/src/duplix/_template/src/solver/allocator_cpsat.py +1250 -0
- duplix-0.1.0/src/duplix/_template/src/staged_overrides.py +315 -0
- duplix-0.1.0/src/duplix/_template/src/state.py +291 -0
- duplix-0.1.0/src/duplix/_template/src/step1_clean_flights.py +301 -0
- duplix-0.1.0/src/duplix/_template/src/step2_extract_roster.py +322 -0
- duplix-0.1.0/src/duplix/_template/src/step3_allocate_flights.py +1617 -0
- duplix-0.1.0/src/duplix/_template/src/web/__init__.py +47 -0
- duplix-0.1.0/src/duplix/_template/src/web/__main__.py +9 -0
- duplix-0.1.0/src/duplix/_template/src/web/api/__init__.py +53 -0
- duplix-0.1.0/src/duplix/_template/src/web/api/export.py +27 -0
- duplix-0.1.0/src/duplix/_template/src/web/api/inputs.py +62 -0
- duplix-0.1.0/src/duplix/_template/src/web/api/override_rows.py +138 -0
- duplix-0.1.0/src/duplix/_template/src/web/api/pages.py +30 -0
- duplix-0.1.0/src/duplix/_template/src/web/api/readbacks.py +71 -0
- duplix-0.1.0/src/duplix/_template/src/web/api/recommender.py +72 -0
- duplix-0.1.0/src/duplix/_template/src/web/api/runs.py +78 -0
- duplix-0.1.0/src/duplix/_template/src/web/api/settings.py +103 -0
- duplix-0.1.0/src/duplix/_template/src/web/core/__init__.py +5 -0
- duplix-0.1.0/src/duplix/_template/src/web/core/responses.py +89 -0
- duplix-0.1.0/src/duplix/_template/src/web/core/router.py +167 -0
- duplix-0.1.0/src/duplix/_template/src/web/core/static_files.py +85 -0
- duplix-0.1.0/src/duplix/_template/src/web/overrides/__init__.py +58 -0
- duplix-0.1.0/src/duplix/_template/src/web/overrides/airports.py +181 -0
- duplix-0.1.0/src/duplix/_template/src/web/overrides/config_yaml.py +21 -0
- duplix-0.1.0/src/duplix/_template/src/web/overrides/filters.py +187 -0
- duplix-0.1.0/src/duplix/_template/src/web/overrides/rows.py +110 -0
- duplix-0.1.0/src/duplix/_template/src/web/readback/__init__.py +65 -0
- duplix-0.1.0/src/duplix/_template/src/web/readback/common.py +68 -0
- duplix-0.1.0/src/duplix/_template/src/web/readback/dashboard.py +86 -0
- duplix-0.1.0/src/duplix/_template/src/web/readback/planning.py +242 -0
- duplix-0.1.0/src/duplix/_template/src/web/readback/session.py +157 -0
- duplix-0.1.0/src/duplix/_template/src/web/readback/tables.py +163 -0
- duplix-0.1.0/src/duplix/_template/src/web/runner.py +168 -0
- duplix-0.1.0/src/duplix/_template/src/web/server.py +180 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/base.css +17 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/components/cards.css +166 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/components/drawer.css +80 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/components/filters.css +40 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/components/forms.css +69 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/components/modal.css +98 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/components/run-pill.css +16 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/components/run-status.css +31 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/components/tables.css +58 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/allocations.css +16 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/chart-pie.css +29 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/chart-shift-summary.css +26 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/chart-trend.css +161 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/chart-workload-dist.css +57 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/coverage.css +48 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/dashboard.css +163 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/inputs.css +80 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/recommender.css +53 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/roster.css +58 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/setup-sidebar.css +42 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/shifts.css +21 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/unallocated.css +65 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/features/workload.css +20 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/layout.css +189 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/css/tokens.css +35 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/index.html +66 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/charts/flight-trend.js +329 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/charts/flights-pie.js +78 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/charts/roster.js +70 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/charts/shift-summary.js +102 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/charts/workload-dist.js +82 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/core/api.js +82 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/core/constants.js +48 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/core/dom.js +36 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/core/store.js +142 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/drawer/airports.js +74 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/drawer/index.js +143 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/drawer/rows.js +294 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/drawer/shift-limits.js +132 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/drawer/staged-forms.js +222 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/main.js +97 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/date-control.js +94 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/handlers.js +100 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/input-slots.js +156 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/inputs.js +27 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/plan.js +54 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/run.js +175 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/staffing.js +68 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/zc-selector.js +77 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/sidebar/index.js +88 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/ui/confirm.js +64 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/ui/layout.js +70 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/ui/nominate.js +86 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/ui/tabs.js +57 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/views/allocations.js +167 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/views/dashboard.js +227 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/views/index.js +32 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/views/pairs.js +109 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/views/unallocated.js +280 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/views/warnings.js +72 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/js/views/workload.js +117 -0
- duplix-0.1.0/src/duplix/_template/src/web/static/styles.css +42 -0
- duplix-0.1.0/src/duplix/_template/start.bat +109 -0
- duplix-0.1.0/src/duplix/core.py +6 -0
duplix-0.1.0/.gitignore
ADDED
duplix-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bhishan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
duplix-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: duplix
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scaffold a new project by copying a bundled template project.
|
|
5
|
+
Project-URL: Homepage, https://github.com/BhishanSharma/duplix
|
|
6
|
+
Project-URL: Issues, https://github.com/BhishanSharma/duplix/issues
|
|
7
|
+
Author-email: Bhishan <you@example.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cli,scaffold,template
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: build; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: twine; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# duplix
|
|
23
|
+
|
|
24
|
+
Scaffold a new project by copying a bundled template project, exactly as it is.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install duplix
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -m duplix my_new_project
|
|
36
|
+
# or, after install:
|
|
37
|
+
duplix my_new_project
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This copies duplix's bundled template folder verbatim into `./my_new_project/`
|
|
41
|
+
(files, code, and structure unchanged - nothing is renamed or rewritten).
|
|
42
|
+
|
|
43
|
+
### Options
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m duplix my_new_project --dir ./somewhere
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install -e ".[dev]"
|
|
53
|
+
pytest
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Publishing
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python -m build
|
|
60
|
+
python -m twine upload dist/*
|
|
61
|
+
```
|
duplix-0.1.0/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# duplix
|
|
2
|
+
|
|
3
|
+
Scaffold a new project by copying a bundled template project, exactly as it is.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install duplix
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python -m duplix my_new_project
|
|
15
|
+
# or, after install:
|
|
16
|
+
duplix my_new_project
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This copies duplix's bundled template folder verbatim into `./my_new_project/`
|
|
20
|
+
(files, code, and structure unchanged - nothing is renamed or rewritten).
|
|
21
|
+
|
|
22
|
+
### Options
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python -m duplix my_new_project --dir ./somewhere
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Development
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -e ".[dev]"
|
|
32
|
+
pytest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Publishing
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m build
|
|
39
|
+
python -m twine upload dist/*
|
|
40
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "duplix"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Scaffold a new project by copying a bundled template project."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Bhishan", email = "you@example.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["scaffold", "template", "cli"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
]
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=7.0",
|
|
27
|
+
"build",
|
|
28
|
+
"twine",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
duplix = "duplix.__main__:main"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/BhishanSharma/duplix"
|
|
36
|
+
Issues = "https://github.com/BhishanSharma/duplix/issues"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/duplix"]
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.sdist]
|
|
42
|
+
include = ["src/duplix/**", "README.md", "LICENSE"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Command-line entry point for duplix.
|
|
2
|
+
|
|
3
|
+
Running `python -m duplix <project_name>` copies duplix's bundled template
|
|
4
|
+
project, as-is, into a new folder named <project_name> in the current
|
|
5
|
+
directory (or --dir). No files are renamed or rewritten - it's a plain
|
|
6
|
+
copy of the template's files and code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import importlib.resources
|
|
13
|
+
import shutil
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_template_dir() -> Path:
|
|
19
|
+
"""Locate the bundled template directory inside the installed package."""
|
|
20
|
+
template = importlib.resources.files("duplix").joinpath("_template")
|
|
21
|
+
return Path(str(template))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def scaffold(project_name: str, target_dir: Path) -> Path:
|
|
25
|
+
dest = target_dir / project_name
|
|
26
|
+
if dest.exists():
|
|
27
|
+
raise FileExistsError(f"'{dest}' already exists.")
|
|
28
|
+
|
|
29
|
+
template_dir = get_template_dir()
|
|
30
|
+
if not template_dir.exists():
|
|
31
|
+
raise FileNotFoundError(
|
|
32
|
+
f"Bundled template not found at '{template_dir}'. "
|
|
33
|
+
"The duplix package may be installed incorrectly."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
shutil.copytree(template_dir, dest)
|
|
37
|
+
return dest
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(argv: list[str] | None = None) -> int:
|
|
41
|
+
parser = argparse.ArgumentParser(
|
|
42
|
+
prog="duplix",
|
|
43
|
+
description="Copy duplix's bundled template project into a new folder.",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument("project_name", help="Name of the new project folder to create")
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"-d", "--dir", default=".", help="Directory to create the project in (default: current dir)"
|
|
48
|
+
)
|
|
49
|
+
args = parser.parse_args(argv)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
dest = scaffold(args.project_name, Path(args.dir).resolve())
|
|
53
|
+
except (FileExistsError, FileNotFoundError) as exc:
|
|
54
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
55
|
+
return 1
|
|
56
|
+
|
|
57
|
+
print(f"Copied template to: {dest}")
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Flight Allocation
|
|
2
|
+
|
|
3
|
+
Engine + web console for IndiGo CLC flight allocation. Python 3.11–3.14,
|
|
4
|
+
no database, no Docker, no Node — the UI is a zero-dependency stdlib
|
|
5
|
+
`http.server` app on `127.0.0.1:8765` with plain HTML/CSS/JS under
|
|
6
|
+
`src/web/static/`.
|
|
7
|
+
|
|
8
|
+
**Everything goes in and comes out through the browser.** The three
|
|
9
|
+
input files are uploaded in the browser and held in the server's memory
|
|
10
|
+
for the session; results are read back as JSON and, if you need a file
|
|
11
|
+
to send on, downloaded as a one-off `.xlsx`. There is no console
|
|
12
|
+
workbook, no `excel/` folder to manage, and no per-date file tree.
|
|
13
|
+
|
|
14
|
+
The files are split by how often they change. The flight schedule is a
|
|
15
|
+
fresh export every morning, so it sits on the dashboard. The two rosters
|
|
16
|
+
cover a whole period and are set once, in the **Setup** sidebar.
|
|
17
|
+
|
|
18
|
+
## Quick start (Windows)
|
|
19
|
+
|
|
20
|
+
1. Double-click `start.bat`.
|
|
21
|
+
2. First run takes ~1 minute: it creates a `.venv` and installs
|
|
22
|
+
`requirements.txt`. Every run after that opens the browser straight
|
|
23
|
+
to http://127.0.0.1:8765/.
|
|
24
|
+
|
|
25
|
+
## Manual setup (Mac/Linux, or to run it yourself)
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
python3.11 -m venv .venv
|
|
29
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
30
|
+
pip install -r requirements.txt
|
|
31
|
+
python -m src.cli # --port 9000 --no-browser also work
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Daily flow
|
|
35
|
+
|
|
36
|
+
0. **Once per period** — open **Setup** (top right) and upload the two
|
|
37
|
+
roster files. They stay loaded for the session, so this is not part
|
|
38
|
+
of the daily flow:
|
|
39
|
+
|
|
40
|
+
| Slot | What it is |
|
|
41
|
+
|---|---|
|
|
42
|
+
| Staff roster | regular crew roster |
|
|
43
|
+
| AM + ZC roster | AM and ZC roster |
|
|
44
|
+
|
|
45
|
+
1. **Upload** today's flight schedule (the SV portal export) on the
|
|
46
|
+
dashboard's *Today's flight schedule* card.
|
|
47
|
+
|
|
48
|
+
Any sheet name works — each reader takes its canonical sheet if
|
|
49
|
+
present and otherwise falls back to the first sheet in the file.
|
|
50
|
+
|
|
51
|
+
2. Check the allocation date (top right). It is prefilled from the
|
|
52
|
+
**server's** date, not the browser's, so a machine on a stale clock
|
|
53
|
+
can't plan the wrong day. Change it to back-fill another date.
|
|
54
|
+
3. Click **Plan** — cleans the flights and extracts the roster, ~3s.
|
|
55
|
+
4. Review the dashboard: per-shift stats, charts, roster by shift,
|
|
56
|
+
staffing recommendation.
|
|
57
|
+
5. Click **Allocate** — CP-SAT solves, typically 30–90s on a first run.
|
|
58
|
+
6. Adjust anything that needs it from the **Override** drawer:
|
|
59
|
+
- `p2f` / `norse` — handler nominations
|
|
60
|
+
- `sick` — pull someone out of the day
|
|
61
|
+
- `change_role` — promote STAFF→ZC, or push someone to AM
|
|
62
|
+
- `max_flights` / `cutoff_time` — per-staff caps
|
|
63
|
+
- add / remove a flight or a staff member
|
|
64
|
+
7. Click **Allocate** again. Prior assignments are pinned and used as a
|
|
65
|
+
CP-SAT warm start, so only the affected staff's flights move and the
|
|
66
|
+
re-solve takes seconds rather than a minute. Rows whose staff changed
|
|
67
|
+
are flagged in the Allocations tab.
|
|
68
|
+
8. **Download .xlsx** when you want a file to email or print.
|
|
69
|
+
|
|
70
|
+
## Session lifetime
|
|
71
|
+
|
|
72
|
+
State lives in the server process. Restarting it clears the uploads,
|
|
73
|
+
the overrides and the results — start again by uploading the three
|
|
74
|
+
files. **Reset** is narrower: it clears the results but keeps your
|
|
75
|
+
uploads and override rows, so you can go straight back to Plan.
|
|
76
|
+
|
|
77
|
+
## Layout
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
src/
|
|
81
|
+
state.py the in-memory store everything reads and writes
|
|
82
|
+
plan.py Plan stage: step 1 + step 2 + the summary
|
|
83
|
+
step1_clean_flights.py clean the schedule into ops classes
|
|
84
|
+
step2_extract_roster.py rosters -> availability
|
|
85
|
+
step3_allocate_flights.py solve, post-passes, outputs
|
|
86
|
+
staged_overrides.py apply drawer mutations to the working data
|
|
87
|
+
recommender_staffing.py per-shift required headcount
|
|
88
|
+
schemas.py the typed row models the whole app shares
|
|
89
|
+
allocator/ eligibility, caps, pairings, post-passes
|
|
90
|
+
solver/ CP-SAT model
|
|
91
|
+
io/
|
|
92
|
+
readers.py parse the uploaded workbooks + override rows
|
|
93
|
+
export.py render the state as a downloadable .xlsx
|
|
94
|
+
web/
|
|
95
|
+
server.py the request loop + launcher (no endpoint logic)
|
|
96
|
+
core/ Response, the Router, static file serving
|
|
97
|
+
api/ one module per concern; each registers routes
|
|
98
|
+
readback/ state -> the JSON the UI renders
|
|
99
|
+
overrides/ override + config.yml mutations
|
|
100
|
+
runner.py runs a stage on a background thread
|
|
101
|
+
static/
|
|
102
|
+
index.html a shell; views bring their own markup
|
|
103
|
+
js/core/ dom, api, store, shared constants
|
|
104
|
+
js/ui/ tab registry, layout mounting, modals
|
|
105
|
+
js/views/ one module per tab (views/index.js registers)
|
|
106
|
+
js/charts/ one module per dashboard chart
|
|
107
|
+
js/panels/ dashboard blocks with their own load cycle
|
|
108
|
+
js/drawer/ the Override drawer and its sections
|
|
109
|
+
js/sidebar/ the Setup sidebar (the set-once roster files)
|
|
110
|
+
css/ partials, listed in styles.css
|
|
111
|
+
configs/
|
|
112
|
+
config.yml column maps, status aliases, solver knobs
|
|
113
|
+
shift_limits.json per-(shift, role) preferred / acceptable / cap
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```stdlib http.server web UI.
|
|
117
|
+
|
|
118
|
+
Routes (JSON unless noted):
|
|
119
|
+
|
|
120
|
+
GET / text/html — single-page app
|
|
121
|
+
GET /static/<path> the JS module / CSS partial tree
|
|
122
|
+
|
|
123
|
+
GET /api/inputs which of the 3 files are uploaded,
|
|
124
|
+
grouped daily (schedule) / setup (rosters)
|
|
125
|
+
POST /api/inputs/<kind> upload one (raw .xlsx body)
|
|
126
|
+
DEL /api/inputs/<kind> drop one
|
|
127
|
+
GET /api/server_date the operating date, per the server's clock
|
|
128
|
+
GET /api/export.xlsx download the current results
|
|
129
|
+
|
|
130
|
+
GET /api/dashboard headline counts + run status
|
|
131
|
+
GET /api/allocations every allocation row
|
|
132
|
+
GET /api/workload per-staff workload summary
|
|
133
|
+
GET /api/pairs pair map
|
|
134
|
+
GET /api/warnings warnings
|
|
135
|
+
GET /api/unallocated unallocated flights
|
|
136
|
+
GET /api/recommendations Phase-R suggestions per unallocated
|
|
137
|
+
GET /api/plan plan summary payload
|
|
138
|
+
GET /api/handlers P2F / NORSE handler picture
|
|
139
|
+
GET /api/staffing staffing recommendation
|
|
140
|
+
GET /api/staff_names roster names for the drawer dropdowns
|
|
141
|
+
GET /api/shift_limits current (shift, role) bands
|
|
142
|
+
|
|
143
|
+
GET /api/overrides override rows
|
|
144
|
+
POST /api/overrides append one {values: [...]}
|
|
145
|
+
PUT /api/overrides/<i> overwrite row i
|
|
146
|
+
DEL /api/overrides/<i> delete row i
|
|
147
|
+
POST /api/overrides/apply_staged apply staged rows to the data
|
|
148
|
+
|
|
149
|
+
POST /api/run {date, step} — plan | all | reset | …
|
|
150
|
+
GET /api/run/status current run state
|
|
151
|
+
|
|
152
|
+
Bind: 127.0.0.1:8765. Everything lives in memory for the life of the
|
|
153
|
+
process; there is no workbook and no per-date file.
|
|
154
|
+
```
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Flight Allocation system — runtime config
|
|
2
|
+
# All maps live here so onboarding a slightly different export shape only
|
|
3
|
+
# requires editing this file, not Python source. See plan §1.5.1.
|
|
4
|
+
|
|
5
|
+
allocation:
|
|
6
|
+
# Cycle year used to resolve year-less roster headers ("Mon,30Mar" -> 30-Mar-<cycle_year>).
|
|
7
|
+
cycle_year: 2026
|
|
8
|
+
|
|
9
|
+
# Default integration-run date. The launcher and CLI take --date to override.
|
|
10
|
+
# Sample SV portal contains only this date; AM roster covers it; staff roster
|
|
11
|
+
# ends 26-Apr so STAFF availability for this D will be empty (W010).
|
|
12
|
+
default_date: 2026-04-30
|
|
13
|
+
|
|
14
|
+
# Step 3 solver parameters.
|
|
15
|
+
solver:
|
|
16
|
+
# 2026-05-28 (user direction — restoring distribution quality): 350s
|
|
17
|
+
# was too aggressive. The solver was reaching FEASIBLE early but the
|
|
18
|
+
# S1b bucket-spread polish phase (which equalizes per-shift staff
|
|
19
|
+
# counts) kept getting cut short, producing -7 / -5 / -19 outliers in
|
|
20
|
+
# otherwise healthy shifts. 700s gives the solver the late-phase time
|
|
21
|
+
# it needs to drive max-min toward 0 across each bucket.
|
|
22
|
+
#
|
|
23
|
+
# Auto-retry (3 attempts in runner.py) is the safety net when a
|
|
24
|
+
# single attempt leaves UNALLOCATED >= 15 — worst-case total
|
|
25
|
+
# wall-clock = 3 * (700s + post-pass) ~ 40 min, ~12 min nominal.
|
|
26
|
+
# Never set to 0 — caused CP-SAT to search indefinitely on a prior
|
|
27
|
+
# attempt (99 min CPU, 4.6 GB RAM).
|
|
28
|
+
#
|
|
29
|
+
# Soft objective drives equality:
|
|
30
|
+
# - S1 below-preferred: 100 per missing flight (pushes count up)
|
|
31
|
+
# - S1 above-preferred: 50 / 500 / 1500 marginal (cap at +1/+2/+3)
|
|
32
|
+
# - S1b bucket-spread: 600 per unit of (max - min) inside bucket
|
|
33
|
+
# (bumped from 400 alongside the time bump — equalization is
|
|
34
|
+
# more aggressive per-second so the budget pays off harder)
|
|
35
|
+
# Result: {23, 23} costs 100 vs {22, 24} costing 1750 — strongly
|
|
36
|
+
# prefers equal pairs over unequal pairs at the same sum.
|
|
37
|
+
max_seconds: 350
|
|
38
|
+
deficit_penalty_lambda: 100 # legacy knob; ignored by lex objective, kept for API compat
|
|
39
|
+
|
|
40
|
+
# State / cycle-history.
|
|
41
|
+
state:
|
|
42
|
+
staleness_max_days: 2 # plan §7.6 W003 threshold
|
|
43
|
+
|
|
44
|
+
# ---------- I/O column maps ----------
|
|
45
|
+
io:
|
|
46
|
+
sv_portal:
|
|
47
|
+
sheet: IN_SVportal # was "Sheet1"; renamed under IN_*-prefix convention
|
|
48
|
+
# 2026-05-14: SV portal export format simplified — fewer columns,
|
|
49
|
+
# different header names. All aliases below (FLT/Flight No., DEP/Origin,
|
|
50
|
+
# ARR/Dest., STD/Dep., TYPE/A/C Type, LOAD/Pax Load) map to the same
|
|
51
|
+
# raw-row field so the reader picks up whichever is present. STA,
|
|
52
|
+
# AC-subtype, and Aircraft Owner are not in the new export — flights
|
|
53
|
+
# default to no-NORSE-owner / blank subtype.
|
|
54
|
+
columns: # source-col -> raw-row field (per §4.3)
|
|
55
|
+
FLT: flight_id
|
|
56
|
+
"Flight No.": flight_id
|
|
57
|
+
DEP: departure
|
|
58
|
+
Origin: departure
|
|
59
|
+
ARR: arrival
|
|
60
|
+
"Dest.": arrival
|
|
61
|
+
STD: dep_time
|
|
62
|
+
"Dep.": dep_time
|
|
63
|
+
STA: arr_time
|
|
64
|
+
TYPE: aircraft_type
|
|
65
|
+
AC: aircraft_subtype
|
|
66
|
+
Aircraft: aircraft_subtype
|
|
67
|
+
# 2026-05-14 new SV portal export: "A/C Type" holds the aircraft
|
|
68
|
+
# MODEL number (321, 789, 320 — what the legacy export called AC).
|
|
69
|
+
# Map to aircraft_subtype, not aircraft_type. aircraft_type
|
|
70
|
+
# (the J/B/K/X ops-class letter) is absent from the new export,
|
|
71
|
+
# so ops_class falls through to date-based DAY/NIGHT classification.
|
|
72
|
+
"A/C Type": aircraft_subtype
|
|
73
|
+
DATE: date
|
|
74
|
+
Date: date
|
|
75
|
+
LOAD: booked_pax_raw
|
|
76
|
+
"Booked Pax": booked_pax_raw
|
|
77
|
+
"Pax Load": booked_pax_raw
|
|
78
|
+
"Aircraft Owner": owner
|
|
79
|
+
date_format: "%m/%d/%Y" # US-style m/d/Y per §1.5.1
|
|
80
|
+
# Step 1 drops rows whose Booked Pax does not match this regex.
|
|
81
|
+
pax_regex: '^\d+/\d+/\d+$'
|
|
82
|
+
# Phase 3 / Change 1 (2026-05-14, INTL overhaul): flights with
|
|
83
|
+
# DEP in this list are DROPPED at Step 1 extraction. The ARR-side
|
|
84
|
+
# pink-highlight machinery was removed in the same amendment;
|
|
85
|
+
# Gulf-3 ARR flights are plain domestic now.
|
|
86
|
+
routing_drop_dep_codes: [AUH, DOH, DXB]
|
|
87
|
+
# International airport list. Step 1 tags a flight with
|
|
88
|
+
# is_international=True if its **DEP** (3-letter code) is in this list
|
|
89
|
+
# (Change 9, 2026-05-14: DEP-side only; was DEP or ARR). Drives H12
|
|
90
|
+
# in Step 4 (newbies excluded) + the Change 4 D-75→STD coverage rule.
|
|
91
|
+
international_airport_codes:
|
|
92
|
+
- ATH
|
|
93
|
+
- ALA
|
|
94
|
+
- BAH
|
|
95
|
+
- BKK
|
|
96
|
+
- CAN
|
|
97
|
+
- CGK
|
|
98
|
+
- CMB
|
|
99
|
+
- DAC
|
|
100
|
+
- DMM
|
|
101
|
+
- DPS
|
|
102
|
+
- FJR
|
|
103
|
+
- GYD
|
|
104
|
+
- HAN
|
|
105
|
+
- HKG
|
|
106
|
+
- HKT
|
|
107
|
+
- JAF
|
|
108
|
+
- JED
|
|
109
|
+
- KBV
|
|
110
|
+
- KTM
|
|
111
|
+
- KUL
|
|
112
|
+
- KWI
|
|
113
|
+
- LGK
|
|
114
|
+
- MCT
|
|
115
|
+
- MED
|
|
116
|
+
- MLE
|
|
117
|
+
- MRU
|
|
118
|
+
- NBO
|
|
119
|
+
- PEN
|
|
120
|
+
- PVG
|
|
121
|
+
- RKT
|
|
122
|
+
- RUH
|
|
123
|
+
- SAI
|
|
124
|
+
- SEZ
|
|
125
|
+
- SGN
|
|
126
|
+
- SHJ
|
|
127
|
+
- SIN
|
|
128
|
+
- TAS
|
|
129
|
+
- TBS
|
|
130
|
+
- ZRH # smoke test
|
|
131
|
+
- GVA
|
|
132
|
+
|
|
133
|
+
staff_roster:
|
|
134
|
+
sheet: IN_Staff # was "Sheet1" — IN_*-prefixed naming since 2026-05-13
|
|
135
|
+
name_column: STAFF NAME # was "Name" — input files use STAFF NAME
|
|
136
|
+
# Header pattern matches "Mon,30Mar" / "Tue,1Apr".
|
|
137
|
+
date_header_format: "%a,%d%b"
|
|
138
|
+
skip_blank_columns: true # blank cols between Name and first date are dropped
|
|
139
|
+
|
|
140
|
+
am_roster:
|
|
141
|
+
sheet_glob: "IN_AM_Roster*" # was "AM Roster*" — IN_*-prefixed naming
|
|
142
|
+
name_column: STAFF NAME # was "NAME" — input files use STAFF NAME
|
|
143
|
+
# Header pattern matches "30-Apr".
|
|
144
|
+
date_header_format: "%d-%b"
|
|
145
|
+
# Number of leading rows after the header to skip (weekday sub-header in row 2).
|
|
146
|
+
subheader_rows_to_skip: 1
|
|
147
|
+
|
|
148
|
+
# ---------- Status normalization ----------
|
|
149
|
+
status:
|
|
150
|
+
# Aliases applied at read time before enum lookup.
|
|
151
|
+
# Note: keys are quoted so YAML 1.1 doesn't parse "OFF" / "ON" as booleans.
|
|
152
|
+
aliases:
|
|
153
|
+
"OFF": F # day off literal -> §4.1 F
|
|
154
|
+
# User direction 2026-05-10: future samples will write ZC entries
|
|
155
|
+
# as ZC/M, ZC/A, ... (current sample uses M/ZC, A/ZC). Both are
|
|
156
|
+
# accepted — the alias maps the new form to the canonical M/ZC.
|
|
157
|
+
"ZC/M": "M/ZC"
|
|
158
|
+
"ZC/A": "A/ZC"
|
|
159
|
+
"ZC/N": "N/ZC"
|
|
160
|
+
"ZC/M1": "M1/ZC"
|
|
161
|
+
"ZC/A1": "A1/ZC"
|
|
162
|
+
|
|
163
|
+
# Recognized shift codes (current_shift will be populated when status is one of these).
|
|
164
|
+
shifts: [M, A, N, M1, A1]
|
|
165
|
+
|
|
166
|
+
# Recognized non-shift, non-assignable statuses (already in §4.1 enum).
|
|
167
|
+
non_shift_recognized: [U, F, "P/L", "C/L", "C/OFF", TR, ""]
|
|
168
|
+
|
|
169
|
+
# Recognized AM/ZC shift variants.
|
|
170
|
+
am_shifts: ["M/IGT", "A/IGT"]
|
|
171
|
+
zc_shifts: ["M/ZC", "A/ZC", "N/ZC", "M1/ZC", "A1/ZC"]
|
|
172
|
+
|
|
173
|
+
# Anything not matched above maps to CrewStatus.OTHER, assignable=False.
|
|
174
|
+
# Common occurrences in real AM data: N/CCC, L&T TRG, *Audit suffixes.
|
|
175
|
+
unrecognized_treatment: OTHER
|
|
176
|
+
|
|
177
|
+
# ---------- Aircraft type -> ops_class lookup ----------
|
|
178
|
+
# Per §4.13: day/night split is by date; other ops_classes (p2f, ferry, norse, test)
|
|
179
|
+
# are derived from aircraft_type. Initial mapping below is a placeholder seeded from
|
|
180
|
+
# the sample's TYPE values (G, H, J, K, P, T, X) — refine as user confirms semantics.
|
|
181
|
+
# Any TYPE not listed falls through to date-based day/night routing.
|
|
182
|
+
ops_class_by_aircraft_type:
|
|
183
|
+
# 2026-05-11 (revised per user): TYPE-letter routing rules.
|
|
184
|
+
# H → P2F (passenger-to-freighter)
|
|
185
|
+
# J → date-based DAY/NIGHT (passenger normal)
|
|
186
|
+
# X → handled in code: dep==arr → TEST, dep!=arr → FERRY
|
|
187
|
+
# B → CHARTER (charter flight, own class)
|
|
188
|
+
# K / P / T / G → FERRY (legacy ferry markers)
|
|
189
|
+
# TEST / FERRY / CHARTER are now allocated as normal flights and
|
|
190
|
+
# counted under workload (no per-staff special cap). The cleaned
|
|
191
|
+
# output groups them on one SpecialOps group for visibility.
|
|
192
|
+
H: p2f
|
|
193
|
+
B: charter
|
|
194
|
+
K: ferry
|
|
195
|
+
P: ferry
|
|
196
|
+
T: ferry
|
|
197
|
+
G: ferry
|
|
198
|
+
# 2026-05-27 (user direction): TYPE=W flights route to special ops
|
|
199
|
+
# (the FERRY group). Previously they fell
|
|
200
|
+
# through to date-based DAY/NIGHT routing and ended up in DayOps.
|
|
201
|
+
W: ferry
|
|
202
|
+
|
|
203
|
+
# Post-pass §3 (user direction 2026-05-12) — when a handler has a P2F
|
|
204
|
+
# flight, the surrounding domestic flights get removed from their list
|
|
205
|
+
# (briefing / paperwork time) and redistributed via §4. Tolerance is
|
|
206
|
+
# the half-width of the matching window around each anchor (D-3h,
|
|
207
|
+
# D-1h, D+20min).
|
|
208
|
+
#
|
|
209
|
+
# 2026-05-28: ``logic_v2`` switches to the union-window-per-chain rule.
|
|
210
|
+
# For each P2F at STD=T, the {owner P2F handler, pre-planner P2F
|
|
211
|
+
# handler} chain loses ALL flights with STD in [T-2h, T+1h]. The P2F
|
|
212
|
+
# post-pass also runs BEFORE the INTL post-pass when this flag is on,
|
|
213
|
+
# so INTL D-75 doesn't remove a flight the P2F window would already
|
|
214
|
+
# free up. Default false in main builds; the "p2f-logic" variant zip
|
|
215
|
+
# ships with this set true.
|
|
216
|
+
p2f_adjustment:
|
|
217
|
+
tolerance_minutes: 30
|
|
218
|
+
logic_v2: false
|
|
219
|
+
|
|
220
|
+
ops_class_norse_owner_codes:
|
|
221
|
+
# NORSE flights are identified by the Aircraft Owner column (per user
|
|
222
|
+
# direction 2026-05-11). Case-insensitive equality match against any
|
|
223
|
+
# code below routes the flight to NORSE. Replaces the legacy
|
|
224
|
+
# (TYPE, Aircraft) compound rule — owners are explicit in the new
|
|
225
|
+
# export, so we no longer triangulate via type+subtype.
|
|
226
|
+
- N0
|
|
227
|
+
- AC-789
|
|
228
|
+
|
|
229
|
+
# ---------- GULF routing (2026-05-19) ----------
|
|
230
|
+
# A flight is GULF when its DEP airport is in the list below OR its
|
|
231
|
+
# Aircraft Owner / TYPE letter equals one of the listed carrier codes
|
|
232
|
+
# (case-insensitive). GULF flights land on the dedicated
|
|
233
|
+
# GULF class so the assigner can see them for reference,
|
|
234
|
+
# but Step 3's allocator does NOT read that sheet — they are never
|
|
235
|
+
# assigned to any staff.
|
|
236
|
+
ops_class_gulf_dep_codes:
|
|
237
|
+
- AUH
|
|
238
|
+
- DOH
|
|
239
|
+
- DXB
|
|
240
|
+
# 2026-05-21 (user direction): owner-based Gulf detection (QR) dropped.
|
|
241
|
+
ops_class_gulf_owner_codes: []
|
|
242
|
+
|
|
243
|
+
# ---------- User-defined extraction filters (2026-05-25) ----------
|
|
244
|
+
# A flight matching ANY filter below is routed to the GULF class
|
|
245
|
+
# (extract-only — no staff allocation). Each filter is a dict with any
|
|
246
|
+
# subset of fields. Empty / missing field = "don't match on that
|
|
247
|
+
# field". Match is AND across specified fields, case-insensitive
|
|
248
|
+
# exact. Example entries:
|
|
249
|
+
# - ac_owner: N0
|
|
250
|
+
# - ac_type: X
|
|
251
|
+
# - dep: BLR
|
|
252
|
+
# arr: COK
|
|
253
|
+
# - custom_header: REG
|
|
254
|
+
# custom_value: VT-IKW
|
|
255
|
+
# Edit via the Override drawer's "Extraction Filters" form or by hand.
|
|
256
|
+
extraction_filters: []
|