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.
- awcs_scripts-1.0.1/.gitignore +7 -0
- awcs_scripts-1.0.1/.gitlab-ci.yml +46 -0
- awcs_scripts-1.0.1/LICENSE +17 -0
- awcs_scripts-1.0.1/PKG-INFO +109 -0
- awcs_scripts-1.0.1/README.md +94 -0
- awcs_scripts-1.0.1/bash/README.md +3 -0
- awcs_scripts-1.0.1/pyproject.toml +34 -0
- awcs_scripts-1.0.1/src/awcs_scripts/__init__.py +3 -0
- awcs_scripts-1.0.1/src/awcs_scripts/api.py +66 -0
- awcs_scripts-1.0.1/src/awcs_scripts/cli.py +264 -0
- awcs_scripts-1.0.1/src/awcs_scripts/config.py +71 -0
- awcs_scripts-1.0.1/src/awcs_scripts/mrmap/__init__.py +1 -0
- awcs_scripts-1.0.1/src/awcs_scripts/mrmap/mrmap-to-bg.py +434 -0
- awcs_scripts-1.0.1/src/awcs_scripts/mrmap/mrmap-to-raster.py +174 -0
- awcs_scripts-1.0.1/tests/test_api.py +149 -0
- awcs_scripts-1.0.1/tests/test_cli.py +31 -0
- awcs_scripts-1.0.1/tests/test_raster.py +8 -0
- awcs_scripts-1.0.1/uv.lock +724 -0
|
@@ -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,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,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()
|