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.
@@ -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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/moveq-cli.svg)](https://pypi.org/project/moveq-cli/)
43
+ [![Python versions](https://img.shields.io/pypi/pyversions/moveq-cli.svg)](https://pypi.org/project/moveq-cli/)
44
+ [![License](https://img.shields.io/pypi/l/moveq-cli.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/moveq-cli.svg)](https://pypi.org/project/moveq-cli/)
8
+ [![Python versions](https://img.shields.io/pypi/pyversions/moveq-cli.svg)](https://pypi.org/project/moveq-cli/)
9
+ [![License](https://img.shields.io/pypi/l/moveq-cli.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """moveq-cli — command-line tool for moveq."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+ from moveq_cli.main import main
3
+
4
+ if __name__ == "__main__":
5
+ sys.exit(main())
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/moveq-cli.svg)](https://pypi.org/project/moveq-cli/)
43
+ [![Python versions](https://img.shields.io/pypi/pyversions/moveq-cli.svg)](https://pypi.org/project/moveq-cli/)
44
+ [![License](https://img.shields.io/pypi/l/moveq-cli.svg)](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,2 @@
1
+ [console_scripts]
2
+ moveq = moveq_cli.main:main
@@ -0,0 +1,7 @@
1
+ moveq>=0.1.0
2
+
3
+ [frames]
4
+ moveq[frames]
5
+
6
+ [test]
7
+ pytest>=7
@@ -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
+