awcs-scripts 1.0.1__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,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ .pytest_cache/
6
+ *.egg-info/
7
+ .cache/
@@ -0,0 +1,46 @@
1
+ stages:
2
+ - test
3
+ - build
4
+ - publish
5
+
6
+ default:
7
+ image: python:3.12-slim
8
+ tags:
9
+ - docker
10
+ before_script:
11
+ - apt-get update -qq && apt-get install -y -qq curl ca-certificates >/dev/null
12
+ - curl -LsSf https://astral.sh/uv/install.sh | sh
13
+ - export PATH="$HOME/.local/bin:$PATH"
14
+ - uv --version
15
+
16
+ variables:
17
+ UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv"
18
+ UV_LINK_MODE: copy
19
+
20
+ test:
21
+ stage: test
22
+ script:
23
+ - uv sync --frozen --group dev
24
+ - uv run pytest
25
+ - uv run awcs help
26
+
27
+ build:
28
+ stage: build
29
+ script:
30
+ - uv build
31
+ artifacts:
32
+ paths:
33
+ - dist/
34
+ expire_in: 1 week
35
+ rules:
36
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
37
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
38
+
39
+ publish:
40
+ stage: publish
41
+ rules:
42
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
43
+ script:
44
+ - uv version "${CI_COMMIT_TAG#v}"
45
+ - uv build
46
+ - uv publish
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 AWCS
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.5
2
+ Name: awcs-scripts
3
+ Version: 1.0.1
4
+ Summary: Command line scripts for WCS maps and the ARMS API
5
+ Project-URL: Repository, https://gitlab.sprydev.ru/automacon/agv/wcs/awcs_scripts
6
+ Author: AWCS
7
+ License: Apache-2.0
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: httpx
11
+ Requires-Dist: numpy
12
+ Requires-Dist: pillow
13
+ Requires-Dist: tifffile
14
+ Description-Content-Type: text/markdown
15
+
16
+ # awcs-scripts
17
+
18
+ Скрипты для карт WCS и части ARMS API. Пакет называется `awcs-scripts`, команда после установки — `awcs`.
19
+
20
+ ## Публикация в PyPI
21
+
22
+ Выкладка происходит только по тегу `vX.Y.Z`. Обычный push и merge request пакет не публикуют: CI прогоняет тесты и собирает wheel. Версию берёт джоба `publish` из тега (`v0.1.0` → `0.1.0`), поле `version` в `pyproject.toml` на публикацию не влияет.
23
+
24
+ Токен в репозиторий не входит. Его нужно завести один раз:
25
+
26
+ 1. На [pypi.org](https://pypi.org) создать API-токен с правом загрузки. Для первой выкладки имя `awcs-scripts` должно быть свободно: проект создаётся при первой успешной загрузке. До этого нужен токен уровня аккаунта, после первой публикации его можно ограничить этим проектом.
27
+ 2. В GitLab, в CI/CD-переменных этого репозитория, добавить `UV_PUBLISH_TOKEN` со значением токена и включить **Masked**. **Protected** включать только если теги `v*` тоже protected: иначе на теге переменная в джобу не попадёт и `uv publish` упадёт без токена.
28
+
29
+ Тег и отправка:
30
+
31
+ ```
32
+ git tag v0.1.0
33
+ git push origin v0.1.0
34
+ ```
35
+
36
+ Репозиторий закрытый, а `uv publish` пишет на публичный PyPI. После зелёного пайплайна wheel доступен по имени пакета.
37
+
38
+ ## Установка
39
+
40
+ ```
41
+ uv tool install awcs-scripts
42
+ ```
43
+
44
+ До первого тега те же подкоманды запускаются из git. Репозиторий закрытый, доступ к GitLab уже должен быть настроен, токен в команду не подставляется:
45
+
46
+ ```
47
+ uvx --from git+https://gitlab.sprydev.ru/automacon/agv/wcs/awcs_scripts.git awcs help
48
+ ```
49
+
50
+ Из клона репозитория, без публикации:
51
+
52
+ ```
53
+ uv tool install .
54
+ ```
55
+
56
+ После этого команда `awcs` доступна в системе. Для разработки в том же клоне:
57
+
58
+ ```
59
+ uv sync
60
+ uv run awcs help
61
+ uv build
62
+ ```
63
+
64
+ ## Удаление
65
+
66
+ Снимает команду `awcs`, поставленную через `uv tool install` из клона или с PyPI:
67
+
68
+ ```
69
+ uv tool uninstall awcs-scripts
70
+ ```
71
+
72
+ Профиль с адресом и ключом эта команда не трогает. Его файл лежит по `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Чтобы убрать и ключ, удалите этот файл.
73
+
74
+ ## Команды
75
+
76
+ ```
77
+ awcs help
78
+ awcs mrmap-to-raster path/to/map.mrmap --out-dir path/to/out
79
+ awcs mrmap-to-bg path/to/map.mrmap path/to/layout.lif --layout_id layoutId --resolution 0.05 --output path/to/layout.out.lif
80
+ awcs login --url https://dev.wcs.automacon.ru --apikey <apikey>
81
+ awcs users list
82
+ awcs users me
83
+ awcs mission_templates list
84
+ awcs robots list
85
+ awcs missions list
86
+ awcs missions create --template_code PickPallet --params "{\"zone\":\"A\"}"
87
+ awcs mission_templates execute PickPallet --params "{\"zone\":\"A\"}"
88
+ awcs projects list
89
+ awcs projects create --lif_json path/to/layout.lif -m MAIRUI -v 1.0.0 --desc "line 1" --sync
90
+ awcs projects activate 12
91
+ ```
92
+
93
+ Разовый запуск после публикации:
94
+
95
+ ```
96
+ uvx --from awcs-scripts awcs help
97
+ uvx --from awcs-scripts awcs mrmap-to-raster path/to/map.mrmap --resolution 0.05 --out-dir path/to/out
98
+ uvx --from awcs-scripts awcs users me
99
+ ```
100
+
101
+ `awcs mrmap-to-raster` пишет PNG, GeoTIFF и JSON. `--resolution` задаёт метры на пиксель, по умолчанию берётся `resolution_2d` из карты. `awcs mrmap-to-bg` подставляет этот растр в `backgroundImage` одного layout. Без `--output` исходный `.lif` перезаписывается.
102
+
103
+ `awcs login` проверяет ключ через `GET /users/me` и только после ответа 200 сохраняет origin и ключ. Профиль лежит вне репозитория: путь из `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Ключ в вывод не печатается. Клиент сам дописывает `/api/v1` и шлёт заголовок `X-Api-Key`.
104
+
105
+ У списков есть `--page`, `--per_page`, `--filter`, `--sort_by`. `awcs missions create` принимает `--template_id` или `--template_code` и необязательный `--params`. `awcs mission_templates execute` отправляет `--params` плоским JSON-объектом. `awcs projects create` загружает LIF как `multipart/form-data`: `-m` это `--manufacturer`, `-v` и `--version` это `--project_version`, `--desc` это `--description`, `--sync` это `--synchronous`.
106
+
107
+ ## Разработка
108
+
109
+ Карта живёт в `src/awcs_scripts/mrmap/`. Имена файлов совпадают с подкомандами: `mrmap-to-raster.py` и `mrmap-to-bg.py`. Будущие shell-скрипты кладутся в `bash/` и тоже вызываются только через `awcs`.
@@ -0,0 +1,94 @@
1
+ # awcs-scripts
2
+
3
+ Скрипты для карт WCS и части ARMS API. Пакет называется `awcs-scripts`, команда после установки — `awcs`.
4
+
5
+ ## Публикация в PyPI
6
+
7
+ Выкладка происходит только по тегу `vX.Y.Z`. Обычный push и merge request пакет не публикуют: CI прогоняет тесты и собирает wheel. Версию берёт джоба `publish` из тега (`v0.1.0` → `0.1.0`), поле `version` в `pyproject.toml` на публикацию не влияет.
8
+
9
+ Токен в репозиторий не входит. Его нужно завести один раз:
10
+
11
+ 1. На [pypi.org](https://pypi.org) создать API-токен с правом загрузки. Для первой выкладки имя `awcs-scripts` должно быть свободно: проект создаётся при первой успешной загрузке. До этого нужен токен уровня аккаунта, после первой публикации его можно ограничить этим проектом.
12
+ 2. В GitLab, в CI/CD-переменных этого репозитория, добавить `UV_PUBLISH_TOKEN` со значением токена и включить **Masked**. **Protected** включать только если теги `v*` тоже protected: иначе на теге переменная в джобу не попадёт и `uv publish` упадёт без токена.
13
+
14
+ Тег и отправка:
15
+
16
+ ```
17
+ git tag v0.1.0
18
+ git push origin v0.1.0
19
+ ```
20
+
21
+ Репозиторий закрытый, а `uv publish` пишет на публичный PyPI. После зелёного пайплайна wheel доступен по имени пакета.
22
+
23
+ ## Установка
24
+
25
+ ```
26
+ uv tool install awcs-scripts
27
+ ```
28
+
29
+ До первого тега те же подкоманды запускаются из git. Репозиторий закрытый, доступ к GitLab уже должен быть настроен, токен в команду не подставляется:
30
+
31
+ ```
32
+ uvx --from git+https://gitlab.sprydev.ru/automacon/agv/wcs/awcs_scripts.git awcs help
33
+ ```
34
+
35
+ Из клона репозитория, без публикации:
36
+
37
+ ```
38
+ uv tool install .
39
+ ```
40
+
41
+ После этого команда `awcs` доступна в системе. Для разработки в том же клоне:
42
+
43
+ ```
44
+ uv sync
45
+ uv run awcs help
46
+ uv build
47
+ ```
48
+
49
+ ## Удаление
50
+
51
+ Снимает команду `awcs`, поставленную через `uv tool install` из клона или с PyPI:
52
+
53
+ ```
54
+ uv tool uninstall awcs-scripts
55
+ ```
56
+
57
+ Профиль с адресом и ключом эта команда не трогает. Его файл лежит по `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Чтобы убрать и ключ, удалите этот файл.
58
+
59
+ ## Команды
60
+
61
+ ```
62
+ awcs help
63
+ awcs mrmap-to-raster path/to/map.mrmap --out-dir path/to/out
64
+ awcs mrmap-to-bg path/to/map.mrmap path/to/layout.lif --layout_id layoutId --resolution 0.05 --output path/to/layout.out.lif
65
+ awcs login --url https://dev.wcs.automacon.ru --apikey <apikey>
66
+ awcs users list
67
+ awcs users me
68
+ awcs mission_templates list
69
+ awcs robots list
70
+ awcs missions list
71
+ awcs missions create --template_code PickPallet --params "{\"zone\":\"A\"}"
72
+ awcs mission_templates execute PickPallet --params "{\"zone\":\"A\"}"
73
+ awcs projects list
74
+ awcs projects create --lif_json path/to/layout.lif -m MAIRUI -v 1.0.0 --desc "line 1" --sync
75
+ awcs projects activate 12
76
+ ```
77
+
78
+ Разовый запуск после публикации:
79
+
80
+ ```
81
+ uvx --from awcs-scripts awcs help
82
+ uvx --from awcs-scripts awcs mrmap-to-raster path/to/map.mrmap --resolution 0.05 --out-dir path/to/out
83
+ uvx --from awcs-scripts awcs users me
84
+ ```
85
+
86
+ `awcs mrmap-to-raster` пишет PNG, GeoTIFF и JSON. `--resolution` задаёт метры на пиксель, по умолчанию берётся `resolution_2d` из карты. `awcs mrmap-to-bg` подставляет этот растр в `backgroundImage` одного layout. Без `--output` исходный `.lif` перезаписывается.
87
+
88
+ `awcs login` проверяет ключ через `GET /users/me` и только после ответа 200 сохраняет origin и ключ. Профиль лежит вне репозитория: путь из `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Ключ в вывод не печатается. Клиент сам дописывает `/api/v1` и шлёт заголовок `X-Api-Key`.
89
+
90
+ У списков есть `--page`, `--per_page`, `--filter`, `--sort_by`. `awcs missions create` принимает `--template_id` или `--template_code` и необязательный `--params`. `awcs mission_templates execute` отправляет `--params` плоским JSON-объектом. `awcs projects create` загружает LIF как `multipart/form-data`: `-m` это `--manufacturer`, `-v` и `--version` это `--project_version`, `--desc` это `--description`, `--sync` это `--synchronous`.
91
+
92
+ ## Разработка
93
+
94
+ Карта живёт в `src/awcs_scripts/mrmap/`. Имена файлов совпадают с подкомандами: `mrmap-to-raster.py` и `mrmap-to-bg.py`. Будущие shell-скрипты кладутся в `bash/` и тоже вызываются только через `awcs`.
@@ -0,0 +1,3 @@
1
+ # Bash-скрипты
2
+
3
+ Сюда кладутся будущие shell-скрипты. Каждый вызывается только как подкоманда `awcs`, отдельная console-script для него не заводится.
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "awcs-scripts"
3
+ version = "1.0.1"
4
+ description = "Command line scripts for WCS maps and the ARMS API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "Apache-2.0" }
8
+ authors = [{ name = "AWCS" }]
9
+ dependencies = [
10
+ "httpx",
11
+ "numpy",
12
+ "pillow",
13
+ "tifffile",
14
+ ]
15
+
16
+ [project.urls]
17
+ Repository = "https://gitlab.sprydev.ru/automacon/agv/wcs/awcs_scripts"
18
+
19
+ [project.scripts]
20
+ awcs = "awcs_scripts.cli:main"
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [dependency-groups]
27
+ dev = ["pytest"]
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/awcs_scripts"]
31
+
32
+ [tool.hatch.build.targets.wheel.force-include]
33
+ "src/awcs_scripts/mrmap/mrmap-to-raster.py" = "awcs_scripts/mrmap/mrmap-to-raster.py"
34
+ "src/awcs_scripts/mrmap/mrmap-to-bg.py" = "awcs_scripts/mrmap/mrmap-to-bg.py"
@@ -0,0 +1,3 @@
1
+ """Scripts for WCS maps and the ARMS API."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,66 @@
1
+ """ARMS HTTP client. The API key is sent as X-Api-Key."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ _API_PREFIX = "/api/v1"
10
+
11
+
12
+ class ApiError(Exception):
13
+ def __init__(self, status_code: int, body: str) -> None:
14
+ self.status_code = status_code
15
+ self.body = body
16
+ super().__init__(f"HTTP {status_code}")
17
+
18
+
19
+ def origin_of(url: str) -> str:
20
+ """Return the site origin. A trailing /api/v1 is stripped so it is not doubled."""
21
+ value = url.strip().rstrip("/")
22
+ if value.endswith(_API_PREFIX):
23
+ value = value[: -len(_API_PREFIX)].rstrip("/")
24
+ return value
25
+
26
+
27
+ class Client:
28
+ def __init__(
29
+ self,
30
+ url: str,
31
+ apikey: str,
32
+ *,
33
+ transport: httpx.BaseTransport | None = None,
34
+ timeout: float = 120,
35
+ ) -> None:
36
+ self.origin = origin_of(url)
37
+ self._client = httpx.Client(
38
+ base_url=self.origin + _API_PREFIX,
39
+ headers={"X-Api-Key": apikey},
40
+ transport=transport,
41
+ timeout=timeout,
42
+ )
43
+
44
+ def close(self) -> None:
45
+ self._client.close()
46
+
47
+ def __enter__(self) -> Client:
48
+ return self
49
+
50
+ def __exit__(self, *args: object) -> None:
51
+ self.close()
52
+
53
+ def request(
54
+ self,
55
+ method: str,
56
+ path: str,
57
+ *,
58
+ params: dict[str, Any] | None = None,
59
+ json: Any = None,
60
+ data: dict[str, Any] | None = None,
61
+ files: dict[str, Any] | None = None,
62
+ ) -> httpx.Response:
63
+ response = self._client.request(method, path, params=params, json=json, data=data, files=files)
64
+ if response.status_code >= 400:
65
+ raise ApiError(response.status_code, response.text)
66
+ return response
@@ -0,0 +1,264 @@
1
+ """Single `awcs` command for map tools and a slice of the ARMS API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import importlib.util
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from urllib.parse import quote
12
+
13
+ import httpx
14
+
15
+ from awcs_scripts.api import ApiError, Client, origin_of
16
+ from awcs_scripts.config import ConfigError, load_profile, save_profile
17
+
18
+ _TOOLS = Path(__file__).resolve().parent / "mrmap"
19
+
20
+
21
+ def _load_tool(filename: str):
22
+ path = _TOOLS / filename
23
+ spec = importlib.util.spec_from_file_location(filename.removesuffix(".py").replace("-", "_"), path)
24
+ if spec is None or spec.loader is None:
25
+ raise ImportError(f"cannot load {path}")
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
28
+ return module
29
+
30
+
31
+ def _fail(message: str) -> None:
32
+ print(message, file=sys.stderr)
33
+ raise SystemExit(1)
34
+
35
+
36
+ def _emit(response: httpx.Response) -> None:
37
+ if not response.content:
38
+ return
39
+ print(json.dumps(response.json(), ensure_ascii=False, indent=2))
40
+
41
+
42
+ def _open_client() -> Client:
43
+ try:
44
+ url, apikey = load_profile()
45
+ except ConfigError as exc:
46
+ _fail(str(exc))
47
+ return Client(url, apikey)
48
+
49
+
50
+ def _params_object(raw: str | None) -> dict[str, Any] | None:
51
+ if raw is None:
52
+ return None
53
+ payload = json.loads(raw)
54
+ if not isinstance(payload, dict):
55
+ _fail("--params must be a JSON object")
56
+ return payload
57
+
58
+
59
+ def _list_params(args: argparse.Namespace) -> dict[str, Any]:
60
+ params: dict[str, Any] = {}
61
+ if args.page is not None:
62
+ params["page"] = args.page
63
+ if args.per_page is not None:
64
+ params["per_page"] = args.per_page
65
+ if args.filter is not None:
66
+ params["filter"] = args.filter
67
+ if args.sort_by is not None:
68
+ params["sort_by"] = args.sort_by
69
+ return params
70
+
71
+
72
+ def _add_list_flags(parser: argparse.ArgumentParser) -> None:
73
+ parser.add_argument("--page", type=int)
74
+ parser.add_argument("--per_page", type=int)
75
+ parser.add_argument("--filter")
76
+ parser.add_argument("--sort_by")
77
+
78
+
79
+ def _cmd_login(args: argparse.Namespace) -> None:
80
+ origin = origin_of(args.url)
81
+ with Client(origin, args.apikey) as client:
82
+ response = client.request("GET", "/users/me")
83
+ path = save_profile(origin, args.apikey)
84
+ _emit(response)
85
+ print(f"saved {path}", file=sys.stderr)
86
+
87
+
88
+ def _cmd_users_list(args: argparse.Namespace) -> None:
89
+ with _open_client() as client:
90
+ _emit(client.request("GET", "/users", params=_list_params(args)))
91
+
92
+
93
+ def _cmd_users_me(_args: argparse.Namespace) -> None:
94
+ with _open_client() as client:
95
+ _emit(client.request("GET", "/users/me"))
96
+
97
+
98
+ def _cmd_templates_list(args: argparse.Namespace) -> None:
99
+ with _open_client() as client:
100
+ _emit(client.request("GET", "/mission_templates", params=_list_params(args)))
101
+
102
+
103
+ def _cmd_templates_execute(args: argparse.Namespace) -> None:
104
+ body = _params_object(args.params)
105
+ if body is None:
106
+ body = {}
107
+ target = quote(args.id_or_code, safe="")
108
+ with _open_client() as client:
109
+ _emit(client.request("POST", f"/mission_templates/{target}/execute", json=body))
110
+
111
+
112
+ def _cmd_robots_list(args: argparse.Namespace) -> None:
113
+ with _open_client() as client:
114
+ _emit(client.request("GET", "/robots", params=_list_params(args)))
115
+
116
+
117
+ def _cmd_missions_list(args: argparse.Namespace) -> None:
118
+ with _open_client() as client:
119
+ _emit(client.request("GET", "/missions", params=_list_params(args)))
120
+
121
+
122
+ def _cmd_missions_create(args: argparse.Namespace) -> None:
123
+ if args.template_id is not None:
124
+ body: dict[str, Any] = {"template_id": args.template_id}
125
+ else:
126
+ body = {"template_code": args.template_code}
127
+ params = _params_object(args.params)
128
+ if params is not None:
129
+ body["params"] = params
130
+ with _open_client() as client:
131
+ _emit(client.request("POST", "/missions", json=body))
132
+
133
+
134
+ def _cmd_projects_list(args: argparse.Namespace) -> None:
135
+ with _open_client() as client:
136
+ _emit(client.request("GET", "/projects", params=_list_params(args)))
137
+
138
+
139
+ def _cmd_projects_create(args: argparse.Namespace) -> None:
140
+ lif_path = args.lif_json.expanduser()
141
+ data = {"manufacturer": args.manufacturer, "project_version": args.project_version}
142
+ if args.description is not None:
143
+ data["description"] = args.description
144
+ params = {"synchronous": "true"} if args.synchronous else None
145
+ with _open_client() as client:
146
+ with lif_path.open("rb") as handle:
147
+ files = {"lif_json": (lif_path.name, handle, "application/json")}
148
+ _emit(client.request("POST", "/projects", params=params, data=data, files=files))
149
+
150
+
151
+ def _cmd_projects_activate(args: argparse.Namespace) -> None:
152
+ with _open_client() as client:
153
+ _emit(client.request("POST", f"/projects/{args.id}/activate"))
154
+
155
+
156
+ _TOOL_COMMANDS = {
157
+ "mrmap-to-raster": "mrmap-to-raster.py",
158
+ "mrmap-to-bg": "mrmap-to-bg.py",
159
+ }
160
+
161
+
162
+ def _add_tool(sub: argparse._SubParsersAction, name: str, help_text: str) -> None:
163
+ # The real argv is handled in main() before parse_args, so --help reaches the tool.
164
+ # The subparser exists so `awcs help` lists the command.
165
+ sub.add_parser(name, help=help_text)
166
+
167
+
168
+ def build_parser() -> argparse.ArgumentParser:
169
+ parser = argparse.ArgumentParser(prog="awcs", description="AWCS API client")
170
+ sub = parser.add_subparsers(dest="command")
171
+
172
+ help_parser = sub.add_parser("help", help="Show this help")
173
+ help_parser.set_defaults(func=lambda _args: parser.print_help())
174
+
175
+ _add_tool(sub, "mrmap-to-raster", "Rasterize a .mrmap to PNG, GeoTIFF, and JSON")
176
+ _add_tool(sub, "mrmap-to-bg", "Set a .mrmap raster as a LIF layout background")
177
+
178
+ login = sub.add_parser("login", help="Check an API key and save the profile")
179
+ login.add_argument("--url", required=True, help="ARMS origin, for example https://dev.wcs.automacon.ru")
180
+ login.add_argument("--apikey", required=True)
181
+ login.set_defaults(func=_cmd_login)
182
+
183
+ users = sub.add_parser("users", help="Users")
184
+ users_sub = users.add_subparsers(dest="users_command", required=True)
185
+ users_list = users_sub.add_parser("list", help="GET /users")
186
+ _add_list_flags(users_list)
187
+ users_list.set_defaults(func=_cmd_users_list)
188
+ users_me = users_sub.add_parser("me", help="GET /users/me")
189
+ users_me.set_defaults(func=_cmd_users_me)
190
+
191
+ templates = sub.add_parser("mission_templates", help="Mission templates")
192
+ templates_sub = templates.add_subparsers(dest="mission_templates_command", required=True)
193
+ templates_list = templates_sub.add_parser("list", help="GET /mission_templates")
194
+ _add_list_flags(templates_list)
195
+ templates_list.set_defaults(func=_cmd_templates_list)
196
+ templates_execute = templates_sub.add_parser("execute", help="POST /mission_templates/{id}/execute")
197
+ templates_execute.add_argument("id_or_code", help="Numeric id or template code")
198
+ templates_execute.add_argument("--params", help="JSON object sent as the request body")
199
+ templates_execute.set_defaults(func=_cmd_templates_execute)
200
+
201
+ robots = sub.add_parser("robots", help="Robots")
202
+ robots_sub = robots.add_subparsers(dest="robots_command", required=True)
203
+ robots_list = robots_sub.add_parser("list", help="GET /robots")
204
+ _add_list_flags(robots_list)
205
+ robots_list.set_defaults(func=_cmd_robots_list)
206
+
207
+ missions = sub.add_parser("missions", help="Missions")
208
+ missions_sub = missions.add_subparsers(dest="missions_command", required=True)
209
+ missions_list = missions_sub.add_parser("list", help="GET /missions")
210
+ _add_list_flags(missions_list)
211
+ missions_list.set_defaults(func=_cmd_missions_list)
212
+ missions_create = missions_sub.add_parser("create", help="POST /missions")
213
+ template = missions_create.add_mutually_exclusive_group(required=True)
214
+ template.add_argument("--template_id", type=int)
215
+ template.add_argument("--template_code")
216
+ missions_create.add_argument("--params", help="JSON object stored in params")
217
+ missions_create.set_defaults(func=_cmd_missions_create)
218
+
219
+ projects = sub.add_parser("projects", help="Navigation projects")
220
+ projects_sub = projects.add_subparsers(dest="projects_command", required=True)
221
+ projects_list = projects_sub.add_parser("list", help="GET /projects")
222
+ _add_list_flags(projects_list)
223
+ projects_list.set_defaults(func=_cmd_projects_list)
224
+ projects_create = projects_sub.add_parser("create", help="POST /projects with a LIF file")
225
+ projects_create.add_argument("--lif_json", type=Path, required=True, help="Path to a .lif file")
226
+ projects_create.add_argument("--manufacturer", "-m", required=True)
227
+ projects_create.add_argument("--project_version", "--version", "-v", required=True, dest="project_version")
228
+ projects_create.add_argument("--description", "--desc", dest="description")
229
+ projects_create.add_argument("--synchronous", "--sync", action="store_true", dest="synchronous")
230
+ projects_create.set_defaults(func=_cmd_projects_create)
231
+ projects_activate = projects_sub.add_parser("activate", help="POST /projects/{id}/activate")
232
+ projects_activate.add_argument("id", type=int)
233
+ projects_activate.set_defaults(func=_cmd_projects_activate)
234
+
235
+ return parser
236
+
237
+
238
+ def main(argv: list[str] | None = None) -> None:
239
+ args_list = list(sys.argv[1:] if argv is None else argv)
240
+ if args_list and args_list[0] in _TOOL_COMMANDS:
241
+ _load_tool(_TOOL_COMMANDS[args_list[0]]).main(args_list[1:])
242
+ return
243
+ parser = build_parser()
244
+ args = parser.parse_args(args_list)
245
+ func = getattr(args, "func", None)
246
+ if func is None:
247
+ parser.print_help()
248
+ return
249
+ try:
250
+ func(args)
251
+ except ApiError as exc:
252
+ _fail(exc.body or f"HTTP {exc.status_code}")
253
+ except ConfigError as exc:
254
+ _fail(str(exc))
255
+ except json.JSONDecodeError as exc:
256
+ _fail(f"invalid JSON: {exc}")
257
+ except httpx.HTTPError as exc:
258
+ _fail(str(exc))
259
+ except OSError as exc:
260
+ _fail(str(exc))
261
+
262
+
263
+ if __name__ == "__main__":
264
+ main()