moveq-cli 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.
- moveq_cli-0.1.0/LICENSE +29 -0
- moveq_cli-0.1.0/PKG-INFO +91 -0
- moveq_cli-0.1.0/README.md +56 -0
- moveq_cli-0.1.0/pyproject.toml +58 -0
- moveq_cli-0.1.0/setup.cfg +4 -0
- moveq_cli-0.1.0/src/moveq_cli/__init__.py +3 -0
- moveq_cli-0.1.0/src/moveq_cli/__main__.py +5 -0
- moveq_cli-0.1.0/src/moveq_cli/main.py +232 -0
- moveq_cli-0.1.0/src/moveq_cli.egg-info/PKG-INFO +91 -0
- moveq_cli-0.1.0/src/moveq_cli.egg-info/SOURCES.txt +13 -0
- moveq_cli-0.1.0/src/moveq_cli.egg-info/dependency_links.txt +1 -0
- moveq_cli-0.1.0/src/moveq_cli.egg-info/entry_points.txt +2 -0
- moveq_cli-0.1.0/src/moveq_cli.egg-info/requires.txt +7 -0
- moveq_cli-0.1.0/src/moveq_cli.egg-info/top_level.txt +1 -0
- moveq_cli-0.1.0/tests/test_main.py +153 -0
moveq_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, moveq contributors
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
+
POSSIBILITY OF SUCH DAMAGE.
|
moveq_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moveq-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line tool for moveq — compute equity metrics from CSV or JSON without writing Python.
|
|
5
|
+
Author-email: Soura <martisoura@gmail.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/SVamseekar/moveq
|
|
8
|
+
Project-URL: Documentation, https://github.com/SVamseekar/moveq/blob/main/docs/getting_started.md
|
|
9
|
+
Project-URL: Repository, https://github.com/SVamseekar/moveq
|
|
10
|
+
Project-URL: Issues, https://github.com/SVamseekar/moveq/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/SVamseekar/moveq/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: transport,equity,accessibility,cli,gini,palma
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: moveq>=0.1.0
|
|
30
|
+
Provides-Extra: frames
|
|
31
|
+
Requires-Dist: moveq[frames]; extra == "frames"
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# moveq-cli
|
|
37
|
+
|
|
38
|
+
Command-line interface for [`moveq`](https://pypi.org/project/moveq/): compute
|
|
39
|
+
equity metrics, composite accessibility scores, and validate country
|
|
40
|
+
harmonization catalogues from CSV or JSON without writing Python.
|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/moveq-cli/)
|
|
43
|
+
[](https://pypi.org/project/moveq-cli/)
|
|
44
|
+
[](https://github.com/SVamseekar/moveq/blob/main/LICENSE)
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
Requires Python 3.10+.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install moveq-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This installs the `moveq` console script and pulls in `moveq` (and therefore
|
|
55
|
+
`moveq-core` and `moveq-catalogue`).
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
### Inequality metrics from CSV
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
moveq gini data.csv --value trips --weight population
|
|
63
|
+
moveq palma data.csv --value trips --weight population
|
|
64
|
+
moveq ci data.csv --value trips --rank deprivation_rank --weight population
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Composite scoring
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
moveq score --terms '{"coverage": 0.8, "evening": 0.5, "frequency": null}' \
|
|
71
|
+
--weights '{"coverage": 0.5, "evening": 0.3, "frequency": 0.2}'
|
|
72
|
+
|
|
73
|
+
moveq score score_config.json --json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Catalogue validation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
moveq catalogue validate country_catalogue.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Documentation
|
|
83
|
+
|
|
84
|
+
- [Getting started & CLI guide](https://github.com/SVamseekar/moveq/blob/main/docs/getting_started.md)
|
|
85
|
+
- [API reference](https://github.com/SVamseekar/moveq/blob/main/docs/api_reference.md)
|
|
86
|
+
- [Source repository](https://github.com/SVamseekar/moveq)
|
|
87
|
+
- [Changelog](https://github.com/SVamseekar/moveq/blob/main/CHANGELOG.md)
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
[BSD 3-Clause](https://github.com/SVamseekar/moveq/blob/main/LICENSE).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# moveq-cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for [`moveq`](https://pypi.org/project/moveq/): compute
|
|
4
|
+
equity metrics, composite accessibility scores, and validate country
|
|
5
|
+
harmonization catalogues from CSV or JSON without writing Python.
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/moveq-cli/)
|
|
8
|
+
[](https://pypi.org/project/moveq-cli/)
|
|
9
|
+
[](https://github.com/SVamseekar/moveq/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Requires Python 3.10+.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install moveq-cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This installs the `moveq` console script and pulls in `moveq` (and therefore
|
|
20
|
+
`moveq-core` and `moveq-catalogue`).
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
### Inequality metrics from CSV
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
moveq gini data.csv --value trips --weight population
|
|
28
|
+
moveq palma data.csv --value trips --weight population
|
|
29
|
+
moveq ci data.csv --value trips --rank deprivation_rank --weight population
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Composite scoring
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
moveq score --terms '{"coverage": 0.8, "evening": 0.5, "frequency": null}' \
|
|
36
|
+
--weights '{"coverage": 0.5, "evening": 0.3, "frequency": 0.2}'
|
|
37
|
+
|
|
38
|
+
moveq score score_config.json --json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Catalogue validation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
moveq catalogue validate country_catalogue.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Documentation
|
|
48
|
+
|
|
49
|
+
- [Getting started & CLI guide](https://github.com/SVamseekar/moveq/blob/main/docs/getting_started.md)
|
|
50
|
+
- [API reference](https://github.com/SVamseekar/moveq/blob/main/docs/api_reference.md)
|
|
51
|
+
- [Source repository](https://github.com/SVamseekar/moveq)
|
|
52
|
+
- [Changelog](https://github.com/SVamseekar/moveq/blob/main/CHANGELOG.md)
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
[BSD 3-Clause](https://github.com/SVamseekar/moveq/blob/main/LICENSE).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "moveq-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Command-line tool for moveq — compute equity metrics from CSV or JSON without writing Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "BSD-3-Clause"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Soura", email = "martisoura@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"transport",
|
|
18
|
+
"equity",
|
|
19
|
+
"accessibility",
|
|
20
|
+
"cli",
|
|
21
|
+
"gini",
|
|
22
|
+
"palma",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Environment :: Console",
|
|
27
|
+
"Intended Audience :: Science/Research",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
33
|
+
"Programming Language :: Python :: 3.10",
|
|
34
|
+
"Programming Language :: Python :: 3.11",
|
|
35
|
+
"Programming Language :: Python :: 3.12",
|
|
36
|
+
"Programming Language :: Python :: 3.13",
|
|
37
|
+
"Topic :: Scientific/Engineering",
|
|
38
|
+
]
|
|
39
|
+
dependencies = [
|
|
40
|
+
"moveq>=0.1.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
frames = ["moveq[frames]"]
|
|
45
|
+
test = ["pytest>=7"]
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
moveq = "moveq_cli.main:main"
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Homepage = "https://github.com/SVamseekar/moveq"
|
|
52
|
+
Documentation = "https://github.com/SVamseekar/moveq/blob/main/docs/getting_started.md"
|
|
53
|
+
Repository = "https://github.com/SVamseekar/moveq"
|
|
54
|
+
Issues = "https://github.com/SVamseekar/moveq/issues"
|
|
55
|
+
Changelog = "https://github.com/SVamseekar/moveq/blob/main/CHANGELOG.md"
|
|
56
|
+
|
|
57
|
+
[tool.setuptools.packages.find]
|
|
58
|
+
where = ["src"]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""``moveq`` command-line tool.
|
|
2
|
+
|
|
3
|
+
moveq gini data.csv --value trips --weight population
|
|
4
|
+
moveq palma data.csv --value trips --weight population
|
|
5
|
+
moveq ci data.csv --value trips --rank deprivation_rank --weight population
|
|
6
|
+
moveq score --terms '{"coverage": 0.8, "evening": 0.5}' --weights '{"coverage": 0.6, "evening": 0.4}'
|
|
7
|
+
moveq score config.json
|
|
8
|
+
moveq catalogue validate config.json
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import csv
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from moveq_catalogue import Catalogue, SectionAction
|
|
23
|
+
from moveq_core import (
|
|
24
|
+
compute_concentration_index,
|
|
25
|
+
compute_gini,
|
|
26
|
+
compute_palma_ratio,
|
|
27
|
+
compute_score,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _read_columns(path: str, columns: list[str]) -> dict[str, np.ndarray]:
|
|
32
|
+
with open(path, newline="", encoding="utf-8") as fh:
|
|
33
|
+
reader = csv.DictReader(fh)
|
|
34
|
+
rows = list(reader)
|
|
35
|
+
if not rows:
|
|
36
|
+
raise ValueError(f"{path} has no data rows")
|
|
37
|
+
missing = [c for c in columns if c not in rows[0]]
|
|
38
|
+
if missing:
|
|
39
|
+
raise ValueError(f"{path} is missing column(s): {', '.join(missing)}")
|
|
40
|
+
return {c: np.array([float(r[c]) for r in rows]) for c in columns}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _cmd_gini(args: argparse.Namespace) -> int:
|
|
44
|
+
cols = _read_columns(args.csv, [args.value, args.weight])
|
|
45
|
+
result = compute_gini(cols[args.value], cols[args.weight])
|
|
46
|
+
print(f"gini: {result:.4f}")
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _cmd_palma(args: argparse.Namespace) -> int:
|
|
51
|
+
cols = _read_columns(args.csv, [args.value, args.weight])
|
|
52
|
+
result = compute_palma_ratio(cols[args.value], cols[args.weight])
|
|
53
|
+
print(f"palma: {result:.4f}")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _cmd_ci(args: argparse.Namespace) -> int:
|
|
58
|
+
cols = _read_columns(args.csv, [args.value, args.rank, args.weight])
|
|
59
|
+
result = compute_concentration_index(cols[args.value], cols[args.rank], cols[args.weight])
|
|
60
|
+
print(f"concentration_index: {result:.4f}")
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _cmd_score(args: argparse.Namespace) -> int:
|
|
65
|
+
terms: dict[str, Any] = {}
|
|
66
|
+
weights: dict[str, float] = {}
|
|
67
|
+
labels: dict[str, str] | None = None
|
|
68
|
+
n_areas: int | None = None
|
|
69
|
+
context: dict[str, str] | None = None
|
|
70
|
+
|
|
71
|
+
if args.config:
|
|
72
|
+
with open(args.config, encoding="utf-8") as fh:
|
|
73
|
+
data = json.load(fh)
|
|
74
|
+
terms = data.get("terms", {})
|
|
75
|
+
weights = data.get("weights", {})
|
|
76
|
+
labels = data.get("labels")
|
|
77
|
+
n_areas = data.get("n_areas")
|
|
78
|
+
context = data.get("context")
|
|
79
|
+
else:
|
|
80
|
+
if not args.terms or not args.weights:
|
|
81
|
+
raise ValueError("Either provide a config file or both --terms and --weights arguments")
|
|
82
|
+
terms = json.loads(args.terms)
|
|
83
|
+
weights = json.loads(args.weights)
|
|
84
|
+
if args.labels:
|
|
85
|
+
labels = json.loads(args.labels)
|
|
86
|
+
if args.context:
|
|
87
|
+
context = json.loads(args.context)
|
|
88
|
+
n_areas = args.n_areas
|
|
89
|
+
|
|
90
|
+
result = compute_score(
|
|
91
|
+
terms=terms,
|
|
92
|
+
weights=weights,
|
|
93
|
+
labels=labels,
|
|
94
|
+
n_areas=n_areas,
|
|
95
|
+
context=context,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if args.json:
|
|
99
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
if result.score is None:
|
|
103
|
+
print("score: None")
|
|
104
|
+
else:
|
|
105
|
+
print(f"score: {result.score:.1f}")
|
|
106
|
+
|
|
107
|
+
if result.note:
|
|
108
|
+
print(f"note: {result.note}")
|
|
109
|
+
|
|
110
|
+
print("components:")
|
|
111
|
+
for comp in result.components:
|
|
112
|
+
if comp.missing:
|
|
113
|
+
print(f" - {comp.id} ({comp.label}): missing, design_weight={comp.design_weight:.2f}")
|
|
114
|
+
else:
|
|
115
|
+
print(
|
|
116
|
+
f" - {comp.id} ({comp.label}): value={comp.value:.4f}, "
|
|
117
|
+
f"design_weight={comp.design_weight:.2f}, weight_used={comp.weight_used:.4f}"
|
|
118
|
+
)
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _cmd_catalogue_validate(args: argparse.Namespace) -> int:
|
|
123
|
+
with open(args.config, encoding="utf-8") as fh:
|
|
124
|
+
data = json.load(fh)
|
|
125
|
+
|
|
126
|
+
country = data.get("country")
|
|
127
|
+
if not country:
|
|
128
|
+
raise ValueError("Catalogue config missing required 'country' field")
|
|
129
|
+
|
|
130
|
+
base_sections = data.get("base_sections")
|
|
131
|
+
if not base_sections or not isinstance(base_sections, list):
|
|
132
|
+
raise ValueError("Catalogue config missing or invalid 'base_sections' list")
|
|
133
|
+
|
|
134
|
+
catalogue = Catalogue(base_sections, country=country)
|
|
135
|
+
|
|
136
|
+
for item in data.get("mappings", []):
|
|
137
|
+
section_id = item.get("section_id")
|
|
138
|
+
action_str = item.get("action")
|
|
139
|
+
if not section_id or not action_str:
|
|
140
|
+
raise ValueError(f"Mapping item missing section_id or action: {item}")
|
|
141
|
+
action = SectionAction(action_str)
|
|
142
|
+
catalogue.register(
|
|
143
|
+
section_id,
|
|
144
|
+
action,
|
|
145
|
+
note=item.get("note"),
|
|
146
|
+
replacement_title=item.get("replacement_title"),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
issues = catalogue.validate()
|
|
150
|
+
|
|
151
|
+
if args.json:
|
|
152
|
+
out = catalogue.to_dict()
|
|
153
|
+
out["issues"] = issues
|
|
154
|
+
out["valid"] = len(issues) == 0
|
|
155
|
+
print(json.dumps(out, indent=2))
|
|
156
|
+
return 0 if not issues else 1
|
|
157
|
+
|
|
158
|
+
summary = catalogue.summary()
|
|
159
|
+
print(f"country: {country}")
|
|
160
|
+
print(f"summary: same={summary['same']}, replace={summary['replace']}, omit={summary['omit']}")
|
|
161
|
+
|
|
162
|
+
if issues:
|
|
163
|
+
print("validation errors:")
|
|
164
|
+
for issue in issues:
|
|
165
|
+
print(f" - {issue}")
|
|
166
|
+
return 1
|
|
167
|
+
|
|
168
|
+
print("status: valid (fully specified)")
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
173
|
+
parser = argparse.ArgumentParser(prog="moveq", description="Transport-equity metrics and catalogue tools.")
|
|
174
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
175
|
+
|
|
176
|
+
# Gini
|
|
177
|
+
gini = sub.add_parser("gini", help="Population-weighted Gini coefficient")
|
|
178
|
+
gini.add_argument("csv", help="Path to input CSV file")
|
|
179
|
+
gini.add_argument("--value", required=True, help="Service-level column")
|
|
180
|
+
gini.add_argument("--weight", required=True, help="Population-weight column")
|
|
181
|
+
gini.set_defaults(func=_cmd_gini)
|
|
182
|
+
|
|
183
|
+
# Palma
|
|
184
|
+
palma = sub.add_parser("palma", help="Palma ratio (top 10%% / bottom 40%%)")
|
|
185
|
+
palma.add_argument("csv", help="Path to input CSV file")
|
|
186
|
+
palma.add_argument("--value", required=True, help="Service-level column")
|
|
187
|
+
palma.add_argument("--weight", required=True, help="Population-weight column")
|
|
188
|
+
palma.set_defaults(func=_cmd_palma)
|
|
189
|
+
|
|
190
|
+
# Concentration Index
|
|
191
|
+
ci = sub.add_parser("ci", help="Wagstaff Concentration Index")
|
|
192
|
+
ci.add_argument("csv", help="Path to input CSV file")
|
|
193
|
+
ci.add_argument("--value", required=True, help="Service-level column")
|
|
194
|
+
ci.add_argument("--rank", required=True, help="Ranking column (e.g. deprivation rank)")
|
|
195
|
+
ci.add_argument("--weight", required=True, help="Population-weight column")
|
|
196
|
+
ci.set_defaults(func=_cmd_ci)
|
|
197
|
+
|
|
198
|
+
# Score
|
|
199
|
+
score = sub.add_parser("score", help="Compute weighted composite score with graceful missing-term handling")
|
|
200
|
+
score.add_argument("config", nargs="?", default=None, help="Path to JSON configuration file")
|
|
201
|
+
score.add_argument("--terms", help="JSON dictionary of terms: e.g. '{\"coverage\": 0.8}'")
|
|
202
|
+
score.add_argument("--weights", help="JSON dictionary of design weights: e.g. '{\"coverage\": 0.5}'")
|
|
203
|
+
score.add_argument("--labels", help="JSON dictionary of human-readable labels")
|
|
204
|
+
score.add_argument("--context", help="JSON dictionary of free-form context")
|
|
205
|
+
score.add_argument("--n-areas", type=int, help="Count of areal units")
|
|
206
|
+
score.add_argument("--json", action="store_true", help="Output machine-readable JSON")
|
|
207
|
+
score.set_defaults(func=_cmd_score)
|
|
208
|
+
|
|
209
|
+
# Catalogue
|
|
210
|
+
cat_parser = sub.add_parser("catalogue", help="Cross-country catalogue harmonization tools")
|
|
211
|
+
cat_sub = cat_parser.add_subparsers(dest="subcommand", required=True)
|
|
212
|
+
|
|
213
|
+
cat_val = cat_sub.add_parser("validate", help="Validate a country's section catalogue against base sections")
|
|
214
|
+
cat_val.add_argument("config", help="Path to JSON catalogue configuration file")
|
|
215
|
+
cat_val.add_argument("--json", action="store_true", help="Output machine-readable JSON")
|
|
216
|
+
cat_val.set_defaults(func=_cmd_catalogue_validate)
|
|
217
|
+
|
|
218
|
+
return parser
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def main(argv: list[str] | None = None) -> int:
|
|
222
|
+
parser = build_parser()
|
|
223
|
+
args = parser.parse_args(argv)
|
|
224
|
+
try:
|
|
225
|
+
return args.func(args)
|
|
226
|
+
except (ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
|
|
227
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
228
|
+
return 1
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
sys.exit(main())
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moveq-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line tool for moveq — compute equity metrics from CSV or JSON without writing Python.
|
|
5
|
+
Author-email: Soura <martisoura@gmail.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/SVamseekar/moveq
|
|
8
|
+
Project-URL: Documentation, https://github.com/SVamseekar/moveq/blob/main/docs/getting_started.md
|
|
9
|
+
Project-URL: Repository, https://github.com/SVamseekar/moveq
|
|
10
|
+
Project-URL: Issues, https://github.com/SVamseekar/moveq/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/SVamseekar/moveq/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: transport,equity,accessibility,cli,gini,palma
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: moveq>=0.1.0
|
|
30
|
+
Provides-Extra: frames
|
|
31
|
+
Requires-Dist: moveq[frames]; extra == "frames"
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# moveq-cli
|
|
37
|
+
|
|
38
|
+
Command-line interface for [`moveq`](https://pypi.org/project/moveq/): compute
|
|
39
|
+
equity metrics, composite accessibility scores, and validate country
|
|
40
|
+
harmonization catalogues from CSV or JSON without writing Python.
|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/moveq-cli/)
|
|
43
|
+
[](https://pypi.org/project/moveq-cli/)
|
|
44
|
+
[](https://github.com/SVamseekar/moveq/blob/main/LICENSE)
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
Requires Python 3.10+.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install moveq-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This installs the `moveq` console script and pulls in `moveq` (and therefore
|
|
55
|
+
`moveq-core` and `moveq-catalogue`).
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
### Inequality metrics from CSV
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
moveq gini data.csv --value trips --weight population
|
|
63
|
+
moveq palma data.csv --value trips --weight population
|
|
64
|
+
moveq ci data.csv --value trips --rank deprivation_rank --weight population
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Composite scoring
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
moveq score --terms '{"coverage": 0.8, "evening": 0.5, "frequency": null}' \
|
|
71
|
+
--weights '{"coverage": 0.5, "evening": 0.3, "frequency": 0.2}'
|
|
72
|
+
|
|
73
|
+
moveq score score_config.json --json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Catalogue validation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
moveq catalogue validate country_catalogue.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Documentation
|
|
83
|
+
|
|
84
|
+
- [Getting started & CLI guide](https://github.com/SVamseekar/moveq/blob/main/docs/getting_started.md)
|
|
85
|
+
- [API reference](https://github.com/SVamseekar/moveq/blob/main/docs/api_reference.md)
|
|
86
|
+
- [Source repository](https://github.com/SVamseekar/moveq)
|
|
87
|
+
- [Changelog](https://github.com/SVamseekar/moveq/blob/main/CHANGELOG.md)
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
[BSD 3-Clause](https://github.com/SVamseekar/moveq/blob/main/LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/moveq_cli/__init__.py
|
|
5
|
+
src/moveq_cli/__main__.py
|
|
6
|
+
src/moveq_cli/main.py
|
|
7
|
+
src/moveq_cli.egg-info/PKG-INFO
|
|
8
|
+
src/moveq_cli.egg-info/SOURCES.txt
|
|
9
|
+
src/moveq_cli.egg-info/dependency_links.txt
|
|
10
|
+
src/moveq_cli.egg-info/entry_points.txt
|
|
11
|
+
src/moveq_cli.egg-info/requires.txt
|
|
12
|
+
src/moveq_cli.egg-info/top_level.txt
|
|
13
|
+
tests/test_main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
moveq_cli
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
from contextlib import redirect_stdout
|
|
5
|
+
|
|
6
|
+
from moveq_cli.main import main
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _write_csv(path, rows, fieldnames):
|
|
10
|
+
with open(path, "w", newline="", encoding="utf-8") as fh:
|
|
11
|
+
writer = csv.DictWriter(fh, fieldnames=fieldnames)
|
|
12
|
+
writer.writeheader()
|
|
13
|
+
writer.writerows(rows)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_gini_command(tmp_path):
|
|
17
|
+
csv_path = tmp_path / "data.csv"
|
|
18
|
+
_write_csv(
|
|
19
|
+
csv_path,
|
|
20
|
+
[{"trips": "10", "population": "100"}, {"trips": "10", "population": "100"}],
|
|
21
|
+
["trips", "population"],
|
|
22
|
+
)
|
|
23
|
+
buf = io.StringIO()
|
|
24
|
+
with redirect_stdout(buf):
|
|
25
|
+
code = main(["gini", str(csv_path), "--value", "trips", "--weight", "population"])
|
|
26
|
+
assert code == 0
|
|
27
|
+
assert "gini: 0.0000" in buf.getvalue()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_palma_command(tmp_path):
|
|
31
|
+
csv_path = tmp_path / "data.csv"
|
|
32
|
+
_write_csv(
|
|
33
|
+
csv_path,
|
|
34
|
+
[{"trips": str(i * 10), "population": "100"} for i in range(1, 11)],
|
|
35
|
+
["trips", "population"],
|
|
36
|
+
)
|
|
37
|
+
buf = io.StringIO()
|
|
38
|
+
with redirect_stdout(buf):
|
|
39
|
+
code = main(["palma", str(csv_path), "--value", "trips", "--weight", "population"])
|
|
40
|
+
assert code == 0
|
|
41
|
+
assert "palma:" in buf.getvalue()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_ci_command(tmp_path):
|
|
45
|
+
csv_path = tmp_path / "data.csv"
|
|
46
|
+
_write_csv(
|
|
47
|
+
csv_path,
|
|
48
|
+
[
|
|
49
|
+
{"trips": "10", "dep_rank": "1", "population": "100"},
|
|
50
|
+
{"trips": "20", "dep_rank": "2", "population": "100"},
|
|
51
|
+
],
|
|
52
|
+
["trips", "dep_rank", "population"],
|
|
53
|
+
)
|
|
54
|
+
buf = io.StringIO()
|
|
55
|
+
with redirect_stdout(buf):
|
|
56
|
+
code = main(["ci", str(csv_path), "--value", "trips", "--rank", "dep_rank", "--weight", "population"])
|
|
57
|
+
assert code == 0
|
|
58
|
+
assert "concentration_index:" in buf.getvalue()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_missing_column_returns_error(tmp_path):
|
|
62
|
+
csv_path = tmp_path / "data.csv"
|
|
63
|
+
_write_csv(csv_path, [{"trips": "10"}], ["trips"])
|
|
64
|
+
code = main(["gini", str(csv_path), "--value", "trips", "--weight", "population"])
|
|
65
|
+
assert code == 1
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_score_inline_args():
|
|
69
|
+
buf = io.StringIO()
|
|
70
|
+
with redirect_stdout(buf):
|
|
71
|
+
code = main([
|
|
72
|
+
"score",
|
|
73
|
+
"--terms",
|
|
74
|
+
'{"coverage": 0.8, "evening": 0.5, "frequency": null}',
|
|
75
|
+
"--weights",
|
|
76
|
+
'{"coverage": 0.5, "evening": 0.3, "frequency": 0.2}',
|
|
77
|
+
])
|
|
78
|
+
assert code == 0
|
|
79
|
+
out = buf.getvalue()
|
|
80
|
+
assert "score: 68.8" in out
|
|
81
|
+
assert "renormalised" in out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_score_json_config(tmp_path):
|
|
85
|
+
config_path = tmp_path / "score_config.json"
|
|
86
|
+
config_path.write_text(
|
|
87
|
+
json.dumps({
|
|
88
|
+
"terms": {"coverage": 0.8, "evening": 0.5},
|
|
89
|
+
"weights": {"coverage": 0.6, "evening": 0.4},
|
|
90
|
+
}),
|
|
91
|
+
encoding="utf-8",
|
|
92
|
+
)
|
|
93
|
+
buf = io.StringIO()
|
|
94
|
+
with redirect_stdout(buf):
|
|
95
|
+
code = main(["score", str(config_path), "--json"])
|
|
96
|
+
assert code == 0
|
|
97
|
+
data = json.loads(buf.getvalue())
|
|
98
|
+
assert data["score"] == 68.0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_score_missing_arguments():
|
|
102
|
+
code = main(["score"])
|
|
103
|
+
assert code == 1
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_catalogue_validate_valid(tmp_path):
|
|
107
|
+
cat_path = tmp_path / "catalogue.json"
|
|
108
|
+
cat_path.write_text(
|
|
109
|
+
json.dumps({
|
|
110
|
+
"country": "france",
|
|
111
|
+
"base_sections": ["s1", "s2", "s3"],
|
|
112
|
+
"mappings": [
|
|
113
|
+
{"section_id": "s1", "action": "same"},
|
|
114
|
+
{"section_id": "s2", "action": "replace", "replacement_title": "Local S2"},
|
|
115
|
+
{"section_id": "s3", "action": "omit", "note": "No census data available"},
|
|
116
|
+
],
|
|
117
|
+
}),
|
|
118
|
+
encoding="utf-8",
|
|
119
|
+
)
|
|
120
|
+
buf = io.StringIO()
|
|
121
|
+
with redirect_stdout(buf):
|
|
122
|
+
code = main(["catalogue", "validate", str(cat_path)])
|
|
123
|
+
assert code == 0
|
|
124
|
+
assert "status: valid (fully specified)" in buf.getvalue()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_catalogue_validate_invalid_unmapped(tmp_path):
|
|
128
|
+
cat_path = tmp_path / "catalogue_unmapped.json"
|
|
129
|
+
cat_path.write_text(
|
|
130
|
+
json.dumps({
|
|
131
|
+
"country": "france",
|
|
132
|
+
"base_sections": ["s1", "s2"],
|
|
133
|
+
"mappings": [
|
|
134
|
+
{"section_id": "s1", "action": "same"},
|
|
135
|
+
],
|
|
136
|
+
}),
|
|
137
|
+
encoding="utf-8",
|
|
138
|
+
)
|
|
139
|
+
buf = io.StringIO()
|
|
140
|
+
with redirect_stdout(buf):
|
|
141
|
+
code = main(["catalogue", "validate", str(cat_path), "--json"])
|
|
142
|
+
assert code == 1
|
|
143
|
+
data = json.loads(buf.getvalue())
|
|
144
|
+
assert data["valid"] is False
|
|
145
|
+
assert len(data["issues"]) == 1
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_help_command():
|
|
149
|
+
try:
|
|
150
|
+
main(["--help"])
|
|
151
|
+
except SystemExit as exc:
|
|
152
|
+
assert exc.code == 0
|
|
153
|
+
|