awcs-scripts 1.0.1__tar.gz → 1.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.
- awcs_scripts-1.1.0/.vscode/tasks.json +42 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/PKG-INFO +81 -42
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/README.md +80 -41
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/pyproject.toml +1 -1
- awcs_scripts-1.1.0/scripts/release.py +141 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/src/awcs_scripts/__init__.py +1 -1
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/src/awcs_scripts/api.py +34 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/src/awcs_scripts/cli.py +59 -1
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/tests/test_api.py +90 -2
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/tests/test_cli.py +10 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/uv.lock +1 -1
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/.gitignore +0 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/.gitlab-ci.yml +0 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/LICENSE +0 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/bash/README.md +0 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/src/awcs_scripts/config.py +0 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/src/awcs_scripts/mrmap/__init__.py +0 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/src/awcs_scripts/mrmap/mrmap-to-bg.py +0 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/src/awcs_scripts/mrmap/mrmap-to-raster.py +0 -0
- {awcs_scripts-1.0.1 → awcs_scripts-1.1.0}/tests/test_raster.py +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.0",
|
|
3
|
+
"tasks": [
|
|
4
|
+
{
|
|
5
|
+
"label": "release",
|
|
6
|
+
"type": "shell",
|
|
7
|
+
"command": "python",
|
|
8
|
+
"args": ["scripts/release.py", "${input:releasePart}"],
|
|
9
|
+
"options": {
|
|
10
|
+
"cwd": "${workspaceFolder}"
|
|
11
|
+
},
|
|
12
|
+
"presentation": {
|
|
13
|
+
"reveal": "always",
|
|
14
|
+
"panel": "shared"
|
|
15
|
+
},
|
|
16
|
+
"problemMatcher": []
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"label": "release (dry-run)",
|
|
20
|
+
"type": "shell",
|
|
21
|
+
"command": "python",
|
|
22
|
+
"args": ["scripts/release.py", "--dry-run"],
|
|
23
|
+
"options": {
|
|
24
|
+
"cwd": "${workspaceFolder}"
|
|
25
|
+
},
|
|
26
|
+
"presentation": {
|
|
27
|
+
"reveal": "always",
|
|
28
|
+
"panel": "shared"
|
|
29
|
+
},
|
|
30
|
+
"problemMatcher": []
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"inputs": [
|
|
34
|
+
{
|
|
35
|
+
"id": "releasePart",
|
|
36
|
+
"type": "pickString",
|
|
37
|
+
"description": "Which part to bump",
|
|
38
|
+
"options": ["patch", "minor", "major"],
|
|
39
|
+
"default": "patch"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: awcs-scripts
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Command line scripts for WCS maps and the ARMS API
|
|
5
5
|
Project-URL: Repository, https://gitlab.sprydev.ru/automacon/agv/wcs/awcs_scripts
|
|
6
6
|
Author: AWCS
|
|
@@ -17,25 +17,9 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
|
|
18
18
|
Скрипты для карт WCS и части ARMS API. Пакет называется `awcs-scripts`, команда после установки — `awcs`.
|
|
19
19
|
|
|
20
|
-
##
|
|
20
|
+
## Использование
|
|
21
21
|
|
|
22
|
-
|
|
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
|
-
## Установка
|
|
22
|
+
### Установка
|
|
39
23
|
|
|
40
24
|
```
|
|
41
25
|
uv tool install awcs-scripts
|
|
@@ -53,57 +37,112 @@ uvx --from git+https://gitlab.sprydev.ru/automacon/agv/wcs/awcs_scripts.git awcs
|
|
|
53
37
|
uv tool install .
|
|
54
38
|
```
|
|
55
39
|
|
|
56
|
-
После этого команда `awcs` доступна в системе.
|
|
40
|
+
После этого команда `awcs` доступна в системе.
|
|
41
|
+
|
|
42
|
+
Разовый запуск после публикации:
|
|
57
43
|
|
|
58
44
|
```
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
45
|
+
uvx --from awcs-scripts awcs help
|
|
46
|
+
uvx --from awcs-scripts awcs mrmap-to-raster path/to/map.mrmap [--resolution 0.05] [--out-dir path/to/out]
|
|
47
|
+
uvx --from awcs-scripts awcs users me
|
|
62
48
|
```
|
|
63
49
|
|
|
64
|
-
|
|
50
|
+
### Команды
|
|
65
51
|
|
|
66
|
-
|
|
52
|
+
#### Основное
|
|
67
53
|
|
|
68
54
|
```
|
|
69
|
-
|
|
55
|
+
awcs help
|
|
56
|
+
awcs version
|
|
70
57
|
```
|
|
71
58
|
|
|
72
|
-
|
|
59
|
+
#### Карты
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
awcs mrmap-to-raster path/to/map.mrmap [--out-dir path/to/out]
|
|
63
|
+
awcs mrmap-to-bg path/to/map.mrmap path/to/layout.lif --layout_id layoutId [--resolution 0.05] [--output path/to/layout.out.lif]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`awcs mrmap-to-raster` пишет PNG, GeoTIFF и JSON. `--resolution` задаёт метры на пиксель, по умолчанию берётся `resolution_2d` из карты. `awcs mrmap-to-bg` подставляет этот растр в `backgroundImage` одного layout. Без `--output` исходный `.lif` перезаписывается.
|
|
73
67
|
|
|
74
|
-
|
|
68
|
+
#### API
|
|
75
69
|
|
|
76
70
|
```
|
|
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
71
|
awcs login --url https://dev.wcs.automacon.ru --apikey <apikey>
|
|
81
72
|
awcs users list
|
|
82
73
|
awcs users me
|
|
83
74
|
awcs mission_templates list
|
|
84
75
|
awcs robots list
|
|
85
76
|
awcs missions list
|
|
86
|
-
awcs missions create --template_code PickPallet --params "{\"zone\":\"A\"}"
|
|
87
|
-
awcs mission_templates execute PickPallet --params "{\"zone\":\"A\"}"
|
|
77
|
+
awcs missions create --template_code PickPallet [--params "{\"zone\":\"A\"}"]
|
|
78
|
+
awcs mission_templates execute PickPallet [--params "{\"zone\":\"A\"}"]
|
|
88
79
|
awcs projects list
|
|
89
|
-
awcs projects create --lif_json path/to/layout.lif -m MAIRUI -v 1.0.0 --desc "line 1" --sync
|
|
80
|
+
awcs projects create --lif_json path/to/layout.lif -m MAIRUI -v 1.0.0 [--desc "line 1"] [--sync]
|
|
81
|
+
awcs projects upload --lif_json path/to/layout.lif -m MAIRUI -v 1.0.0 [--desc "line 1"] [--sync]
|
|
82
|
+
awcs projects download
|
|
83
|
+
awcs projects download 12 -o path/to/layout.lif
|
|
90
84
|
awcs projects activate 12
|
|
91
85
|
```
|
|
92
86
|
|
|
93
|
-
|
|
87
|
+
`awcs login` проверяет ключ через `GET /users/me` и только после ответа 200 сохраняет origin и ключ. Профиль лежит вне репозитория: путь из `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Ключ в вывод не печатается. Клиент сам дописывает `/api/v1` и шлёт заголовок `X-Api-Key`.
|
|
88
|
+
|
|
89
|
+
У списков есть `--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`. `awcs projects upload` делает то же самое. `awcs projects download` без id забирает активный проект (`GET /projects/active`), с id — этот проект, и сохраняет исходный LIF. `-o` задаёт файл или существующую папку; без него файл пишется в текущий каталог под именем вложения. Путь печатается в stdout.
|
|
90
|
+
|
|
91
|
+
### Удаление
|
|
92
|
+
|
|
93
|
+
Снимает команду `awcs`, поставленную через `uv tool install` из клона или с PyPI:
|
|
94
94
|
|
|
95
95
|
```
|
|
96
|
-
|
|
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
|
|
96
|
+
uv tool uninstall awcs-scripts
|
|
99
97
|
```
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
Профиль с адресом и ключом эта команда не трогает. Его файл лежит по `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Чтобы убрать и ключ, удалите этот файл.
|
|
102
100
|
|
|
103
|
-
|
|
101
|
+
## Разработка
|
|
104
102
|
|
|
105
|
-
|
|
103
|
+
### Публикация в PyPI
|
|
106
104
|
|
|
107
|
-
|
|
105
|
+
Выкладка происходит только по тегу `vX.Y.Z`. Обычный push и merge request пакет не публикуют: CI прогоняет тесты и собирает wheel. Как сменить номер версии — в разделе ниже.
|
|
106
|
+
|
|
107
|
+
Токен в репозиторий не входит. Его нужно завести один раз:
|
|
108
|
+
|
|
109
|
+
1. На [pypi.org](https://pypi.org) создать API-токен с правом загрузки. Для первой выкладки имя `awcs-scripts` должно быть свободно: проект создаётся при первой успешной загрузке. До этого нужен токен уровня аккаунта, после первой публикации его можно ограничить этим проектом.
|
|
110
|
+
2. В GitLab, в CI/CD-переменных этого репозитория, добавить `UV_PUBLISH_TOKEN` со значением токена и включить **Masked**. **Protected** включать только если теги `v*` тоже protected: иначе на теге переменная в джобу не попадёт и `uv publish` упадёт без токена.
|
|
111
|
+
|
|
112
|
+
Репозиторий закрытый, а `uv publish` пишет на публичный PyPI. После зелёного пайплайна wheel доступен по имени пакета.
|
|
113
|
+
|
|
114
|
+
### Обновление версии
|
|
115
|
+
|
|
116
|
+
Выпуск — одна команда из корня репозитория. По умолчанию она поднимает patch:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
python scripts/release.py
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`minor` и `major` передаются аргументом: `python scripts/release.py minor`. Команда берёт последний тег `vX.Y.Z`, при другом номере в `pyproject.toml` сначала записывает туда версию тега, затем выполняет `uv version --bump`. Тот же номер попадает в `__version__` в `src/awcs_scripts/__init__.py` и, если uv его перепишет, в `uv.lock`. Дальше идут коммит `Release X.Y.Z.`, тег `vX.Y.Z`, `git push origin HEAD` и `git push origin vX.Y.Z`.
|
|
123
|
+
|
|
124
|
+
Посмотреть следующий номер:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
python scripts/release.py --dry-run
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
В Cursor это задачи `release` и `release (dry-run)`: Command Palette → Tasks: Run Task. Команда выпускает из чистого дерева на ветке. Тег должен совпадать с `^v\d+\.\d+\.\d+$`: по нему джоба `publish` подставляет номер командой `uv version` и собирает wheel. PyPI не принимает повторную загрузку той же версии.
|
|
131
|
+
|
|
132
|
+
Уже установленную команду обновляют после зелёного пайплайна тега:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
uv tool upgrade awcs-scripts
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### В клоне
|
|
139
|
+
|
|
140
|
+
Для разработки в том же клоне:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
uv sync
|
|
144
|
+
uv run awcs help
|
|
145
|
+
uv build
|
|
146
|
+
```
|
|
108
147
|
|
|
109
148
|
Карта живёт в `src/awcs_scripts/mrmap/`. Имена файлов совпадают с подкомандами: `mrmap-to-raster.py` и `mrmap-to-bg.py`. Будущие shell-скрипты кладутся в `bash/` и тоже вызываются только через `awcs`.
|
|
@@ -2,25 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Скрипты для карт WCS и части ARMS API. Пакет называется `awcs-scripts`, команда после установки — `awcs`.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Использование
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
## Установка
|
|
7
|
+
### Установка
|
|
24
8
|
|
|
25
9
|
```
|
|
26
10
|
uv tool install awcs-scripts
|
|
@@ -38,57 +22,112 @@ uvx --from git+https://gitlab.sprydev.ru/automacon/agv/wcs/awcs_scripts.git awcs
|
|
|
38
22
|
uv tool install .
|
|
39
23
|
```
|
|
40
24
|
|
|
41
|
-
После этого команда `awcs` доступна в системе.
|
|
25
|
+
После этого команда `awcs` доступна в системе.
|
|
26
|
+
|
|
27
|
+
Разовый запуск после публикации:
|
|
42
28
|
|
|
43
29
|
```
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
30
|
+
uvx --from awcs-scripts awcs help
|
|
31
|
+
uvx --from awcs-scripts awcs mrmap-to-raster path/to/map.mrmap [--resolution 0.05] [--out-dir path/to/out]
|
|
32
|
+
uvx --from awcs-scripts awcs users me
|
|
47
33
|
```
|
|
48
34
|
|
|
49
|
-
|
|
35
|
+
### Команды
|
|
50
36
|
|
|
51
|
-
|
|
37
|
+
#### Основное
|
|
52
38
|
|
|
53
39
|
```
|
|
54
|
-
|
|
40
|
+
awcs help
|
|
41
|
+
awcs version
|
|
55
42
|
```
|
|
56
43
|
|
|
57
|
-
|
|
44
|
+
#### Карты
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
awcs mrmap-to-raster path/to/map.mrmap [--out-dir path/to/out]
|
|
48
|
+
awcs mrmap-to-bg path/to/map.mrmap path/to/layout.lif --layout_id layoutId [--resolution 0.05] [--output path/to/layout.out.lif]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`awcs mrmap-to-raster` пишет PNG, GeoTIFF и JSON. `--resolution` задаёт метры на пиксель, по умолчанию берётся `resolution_2d` из карты. `awcs mrmap-to-bg` подставляет этот растр в `backgroundImage` одного layout. Без `--output` исходный `.lif` перезаписывается.
|
|
58
52
|
|
|
59
|
-
|
|
53
|
+
#### API
|
|
60
54
|
|
|
61
55
|
```
|
|
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
56
|
awcs login --url https://dev.wcs.automacon.ru --apikey <apikey>
|
|
66
57
|
awcs users list
|
|
67
58
|
awcs users me
|
|
68
59
|
awcs mission_templates list
|
|
69
60
|
awcs robots list
|
|
70
61
|
awcs missions list
|
|
71
|
-
awcs missions create --template_code PickPallet --params "{\"zone\":\"A\"}"
|
|
72
|
-
awcs mission_templates execute PickPallet --params "{\"zone\":\"A\"}"
|
|
62
|
+
awcs missions create --template_code PickPallet [--params "{\"zone\":\"A\"}"]
|
|
63
|
+
awcs mission_templates execute PickPallet [--params "{\"zone\":\"A\"}"]
|
|
73
64
|
awcs projects list
|
|
74
|
-
awcs projects create --lif_json path/to/layout.lif -m MAIRUI -v 1.0.0 --desc "line 1" --sync
|
|
65
|
+
awcs projects create --lif_json path/to/layout.lif -m MAIRUI -v 1.0.0 [--desc "line 1"] [--sync]
|
|
66
|
+
awcs projects upload --lif_json path/to/layout.lif -m MAIRUI -v 1.0.0 [--desc "line 1"] [--sync]
|
|
67
|
+
awcs projects download
|
|
68
|
+
awcs projects download 12 -o path/to/layout.lif
|
|
75
69
|
awcs projects activate 12
|
|
76
70
|
```
|
|
77
71
|
|
|
78
|
-
|
|
72
|
+
`awcs login` проверяет ключ через `GET /users/me` и только после ответа 200 сохраняет origin и ключ. Профиль лежит вне репозитория: путь из `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Ключ в вывод не печатается. Клиент сам дописывает `/api/v1` и шлёт заголовок `X-Api-Key`.
|
|
73
|
+
|
|
74
|
+
У списков есть `--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`. `awcs projects upload` делает то же самое. `awcs projects download` без id забирает активный проект (`GET /projects/active`), с id — этот проект, и сохраняет исходный LIF. `-o` задаёт файл или существующую папку; без него файл пишется в текущий каталог под именем вложения. Путь печатается в stdout.
|
|
75
|
+
|
|
76
|
+
### Удаление
|
|
77
|
+
|
|
78
|
+
Снимает команду `awcs`, поставленную через `uv tool install` из клона или с PyPI:
|
|
79
79
|
|
|
80
80
|
```
|
|
81
|
-
|
|
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
|
|
81
|
+
uv tool uninstall awcs-scripts
|
|
84
82
|
```
|
|
85
83
|
|
|
86
|
-
|
|
84
|
+
Профиль с адресом и ключом эта команда не трогает. Его файл лежит по `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Чтобы убрать и ключ, удалите этот файл.
|
|
87
85
|
|
|
88
|
-
|
|
86
|
+
## Разработка
|
|
89
87
|
|
|
90
|
-
|
|
88
|
+
### Публикация в PyPI
|
|
91
89
|
|
|
92
|
-
|
|
90
|
+
Выкладка происходит только по тегу `vX.Y.Z`. Обычный push и merge request пакет не публикуют: CI прогоняет тесты и собирает wheel. Как сменить номер версии — в разделе ниже.
|
|
91
|
+
|
|
92
|
+
Токен в репозиторий не входит. Его нужно завести один раз:
|
|
93
|
+
|
|
94
|
+
1. На [pypi.org](https://pypi.org) создать API-токен с правом загрузки. Для первой выкладки имя `awcs-scripts` должно быть свободно: проект создаётся при первой успешной загрузке. До этого нужен токен уровня аккаунта, после первой публикации его можно ограничить этим проектом.
|
|
95
|
+
2. В GitLab, в CI/CD-переменных этого репозитория, добавить `UV_PUBLISH_TOKEN` со значением токена и включить **Masked**. **Protected** включать только если теги `v*` тоже protected: иначе на теге переменная в джобу не попадёт и `uv publish` упадёт без токена.
|
|
96
|
+
|
|
97
|
+
Репозиторий закрытый, а `uv publish` пишет на публичный PyPI. После зелёного пайплайна wheel доступен по имени пакета.
|
|
98
|
+
|
|
99
|
+
### Обновление версии
|
|
100
|
+
|
|
101
|
+
Выпуск — одна команда из корня репозитория. По умолчанию она поднимает patch:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
python scripts/release.py
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`minor` и `major` передаются аргументом: `python scripts/release.py minor`. Команда берёт последний тег `vX.Y.Z`, при другом номере в `pyproject.toml` сначала записывает туда версию тега, затем выполняет `uv version --bump`. Тот же номер попадает в `__version__` в `src/awcs_scripts/__init__.py` и, если uv его перепишет, в `uv.lock`. Дальше идут коммит `Release X.Y.Z.`, тег `vX.Y.Z`, `git push origin HEAD` и `git push origin vX.Y.Z`.
|
|
108
|
+
|
|
109
|
+
Посмотреть следующий номер:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
python scripts/release.py --dry-run
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
В Cursor это задачи `release` и `release (dry-run)`: Command Palette → Tasks: Run Task. Команда выпускает из чистого дерева на ветке. Тег должен совпадать с `^v\d+\.\d+\.\d+$`: по нему джоба `publish` подставляет номер командой `uv version` и собирает wheel. PyPI не принимает повторную загрузку той же версии.
|
|
116
|
+
|
|
117
|
+
Уже установленную команду обновляют после зелёного пайплайна тега:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
uv tool upgrade awcs-scripts
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### В клоне
|
|
124
|
+
|
|
125
|
+
Для разработки в том же клоне:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
uv sync
|
|
129
|
+
uv run awcs help
|
|
130
|
+
uv build
|
|
131
|
+
```
|
|
93
132
|
|
|
94
133
|
Карта живёт в `src/awcs_scripts/mrmap/`. Имена файлов совпадают с подкомандами: `mrmap-to-raster.py` и `mrmap-to-bg.py`. Будущие shell-скрипты кладутся в `bash/` и тоже вызываются только через `awcs`.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Bump the package version, commit it, tag vX.Y.Z, and push the branch and tag."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
12
|
+
PYPROJECT = ROOT / "pyproject.toml"
|
|
13
|
+
INIT = ROOT / "src" / "awcs_scripts" / "__init__.py"
|
|
14
|
+
VERSION_FILES = (
|
|
15
|
+
"pyproject.toml",
|
|
16
|
+
"src/awcs_scripts/__init__.py",
|
|
17
|
+
"uv.lock",
|
|
18
|
+
)
|
|
19
|
+
TAG = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$")
|
|
20
|
+
PARTS = ("major", "minor", "patch")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _fail(message: str) -> None:
|
|
24
|
+
print(message, file=sys.stderr)
|
|
25
|
+
raise SystemExit(1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _run(cmd: list[str]) -> subprocess.CompletedProcess[str]:
|
|
29
|
+
result = subprocess.run(cmd, cwd=ROOT, text=True, capture_output=True, check=False)
|
|
30
|
+
if result.returncode != 0:
|
|
31
|
+
detail = (result.stderr or result.stdout).strip()
|
|
32
|
+
_fail(detail or f"command failed: {' '.join(cmd)}")
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _git(*args: str) -> str:
|
|
37
|
+
return _run(["git", *args]).stdout.strip()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _pyproject_version() -> str:
|
|
41
|
+
text = PYPROJECT.read_text(encoding="utf-8")
|
|
42
|
+
match = re.search(r'(?m)^version = "(\d+\.\d+\.\d+)"\s*$', text)
|
|
43
|
+
if match is None:
|
|
44
|
+
_fail("version in pyproject.toml is not X.Y.Z")
|
|
45
|
+
return match.group(1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _latest_tag_version() -> str | None:
|
|
49
|
+
for line in _git("tag", "-l", "--sort=-v:refname").splitlines():
|
|
50
|
+
if TAG.fullmatch(line.strip()):
|
|
51
|
+
return line.strip()[1:]
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _bump(version: str, part: str) -> str:
|
|
56
|
+
major, minor, patch = (int(piece) for piece in version.split("."))
|
|
57
|
+
if part == "major":
|
|
58
|
+
return f"{major + 1}.0.0"
|
|
59
|
+
if part == "minor":
|
|
60
|
+
return f"{major}.{minor + 1}.0"
|
|
61
|
+
return f"{major}.{minor}.{patch + 1}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _status_paths() -> list[str]:
|
|
65
|
+
# Keep the leading space in porcelain lines. strip() would turn
|
|
66
|
+
# " M pyproject.toml" into "M pyproject.toml" and drop the first letter.
|
|
67
|
+
paths: list[str] = []
|
|
68
|
+
for line in _run(["git", "status", "--porcelain"]).stdout.splitlines():
|
|
69
|
+
path = line[3:]
|
|
70
|
+
if " -> " in path:
|
|
71
|
+
path = path.split(" -> ", 1)[1]
|
|
72
|
+
paths.append(path.strip().strip('"'))
|
|
73
|
+
return paths
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _require_branch() -> None:
|
|
77
|
+
if _git("rev-parse", "--abbrev-ref", "HEAD") == "HEAD":
|
|
78
|
+
_fail("HEAD is detached; checkout a branch before releasing")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _require_clean() -> None:
|
|
82
|
+
if _status_paths():
|
|
83
|
+
_fail("working tree is not clean; commit other changes before releasing")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _write_version(version: str) -> None:
|
|
87
|
+
with INIT.open(encoding="utf-8", newline="") as handle:
|
|
88
|
+
text = handle.read()
|
|
89
|
+
updated, count = re.subn(
|
|
90
|
+
r'^__version__ = "[^"]*"',
|
|
91
|
+
f'__version__ = "{version}"',
|
|
92
|
+
text,
|
|
93
|
+
count=1,
|
|
94
|
+
flags=re.M,
|
|
95
|
+
)
|
|
96
|
+
if count != 1:
|
|
97
|
+
_fail("__version__ not found in src/awcs_scripts/__init__.py")
|
|
98
|
+
with INIT.open("w", encoding="utf-8", newline="") as handle:
|
|
99
|
+
handle.write(updated)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(argv: list[str] | None = None) -> None:
|
|
103
|
+
parser = argparse.ArgumentParser(description="Bump, tag, and push a release.")
|
|
104
|
+
parser.add_argument("part", nargs="?", default="patch", choices=PARTS)
|
|
105
|
+
parser.add_argument("--dry-run", action="store_true", help="Print the next version and write nothing")
|
|
106
|
+
args = parser.parse_args(argv)
|
|
107
|
+
|
|
108
|
+
base = _latest_tag_version() or _pyproject_version()
|
|
109
|
+
version = _bump(base, args.part)
|
|
110
|
+
if args.dry_run:
|
|
111
|
+
print(version)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
_require_branch()
|
|
115
|
+
_require_clean()
|
|
116
|
+
if _git("tag", "-l", f"v{version}"):
|
|
117
|
+
_fail(f"tag v{version} already exists")
|
|
118
|
+
|
|
119
|
+
if _pyproject_version() != base:
|
|
120
|
+
_run(["uv", "version", base, "--no-sync"])
|
|
121
|
+
bumped = _run(["uv", "version", "--bump", args.part, "--short", "--no-sync"]).stdout.strip()
|
|
122
|
+
if bumped != version:
|
|
123
|
+
_fail(f"uv version returned {bumped}, expected {version}")
|
|
124
|
+
_write_version(version)
|
|
125
|
+
|
|
126
|
+
unexpected = [path for path in _status_paths() if path not in VERSION_FILES]
|
|
127
|
+
if unexpected:
|
|
128
|
+
_fail("unexpected changes: " + ", ".join(unexpected))
|
|
129
|
+
if not _status_paths():
|
|
130
|
+
_fail("version did not change")
|
|
131
|
+
|
|
132
|
+
_run(["git", "add", "--", *VERSION_FILES])
|
|
133
|
+
_run(["git", "commit", "-m", f"Release {version}."])
|
|
134
|
+
_run(["git", "tag", f"v{version}"])
|
|
135
|
+
_run(["git", "push", "origin", "HEAD"])
|
|
136
|
+
_run(["git", "push", "origin", f"v{version}"])
|
|
137
|
+
print(f"v{version}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
main()
|
|
@@ -24,6 +24,13 @@ def origin_of(url: str) -> str:
|
|
|
24
24
|
return value
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def _strip_api_prefix(path: str) -> str:
|
|
28
|
+
"""Drop a leading /api/v1 so the client base URL does not double it."""
|
|
29
|
+
if path.startswith(_API_PREFIX + "/"):
|
|
30
|
+
return path[len(_API_PREFIX) :]
|
|
31
|
+
return path
|
|
32
|
+
|
|
33
|
+
|
|
27
34
|
class Client:
|
|
28
35
|
def __init__(
|
|
29
36
|
self,
|
|
@@ -64,3 +71,30 @@ class Client:
|
|
|
64
71
|
if response.status_code >= 400:
|
|
65
72
|
raise ApiError(response.status_code, response.text)
|
|
66
73
|
return response
|
|
74
|
+
|
|
75
|
+
def get(self, url_or_path: str) -> httpx.Response:
|
|
76
|
+
"""GET an API path or an absolute URL.
|
|
77
|
+
|
|
78
|
+
A leading /api/v1 is removed so the client base URL does not double it.
|
|
79
|
+
An absolute URL on this origin is sent with the API key. An absolute URL
|
|
80
|
+
on another host is fetched without the key.
|
|
81
|
+
"""
|
|
82
|
+
if url_or_path.startswith(("http://", "https://")):
|
|
83
|
+
return self._get_absolute(url_or_path)
|
|
84
|
+
return self.request("GET", _strip_api_prefix(url_or_path))
|
|
85
|
+
|
|
86
|
+
def _get_absolute(self, url: str) -> httpx.Response:
|
|
87
|
+
target = httpx.URL(url)
|
|
88
|
+
origin = httpx.URL(self.origin)
|
|
89
|
+
same_origin = (
|
|
90
|
+
target.scheme == origin.scheme
|
|
91
|
+
and target.host == origin.host
|
|
92
|
+
and target.port == origin.port
|
|
93
|
+
)
|
|
94
|
+
if not same_origin:
|
|
95
|
+
response = httpx.get(url, timeout=self._client.timeout, follow_redirects=True)
|
|
96
|
+
if response.status_code >= 400:
|
|
97
|
+
raise ApiError(response.status_code, response.text)
|
|
98
|
+
return response
|
|
99
|
+
params = dict(target.params) or None
|
|
100
|
+
return self.request("GET", _strip_api_prefix(target.path), params=params)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import importlib.metadata
|
|
6
7
|
import importlib.util
|
|
7
8
|
import json
|
|
8
9
|
import sys
|
|
@@ -28,6 +29,10 @@ def _load_tool(filename: str):
|
|
|
28
29
|
return module
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
def _cmd_version(_args: argparse.Namespace) -> None:
|
|
33
|
+
print(importlib.metadata.version("awcs-scripts"))
|
|
34
|
+
|
|
35
|
+
|
|
31
36
|
def _fail(message: str) -> None:
|
|
32
37
|
print(message, file=sys.stderr)
|
|
33
38
|
raise SystemExit(1)
|
|
@@ -153,6 +158,45 @@ def _cmd_projects_activate(args: argparse.Namespace) -> None:
|
|
|
153
158
|
_emit(client.request("POST", f"/projects/{args.id}/activate"))
|
|
154
159
|
|
|
155
160
|
|
|
161
|
+
def _download_name(source: dict[str, Any], project_id: object) -> str:
|
|
162
|
+
raw = str(source.get("filename") or "").replace("\\", "/")
|
|
163
|
+
name = Path(raw).name
|
|
164
|
+
if name and name not in {".", ".."}:
|
|
165
|
+
return name
|
|
166
|
+
return f"project-{project_id}.lif"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _download_destination(output: Path | None, filename: str) -> Path:
|
|
170
|
+
if output is None:
|
|
171
|
+
return Path(filename)
|
|
172
|
+
output = output.expanduser()
|
|
173
|
+
if output.is_dir():
|
|
174
|
+
return output / filename
|
|
175
|
+
return output
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _cmd_projects_download(args: argparse.Namespace) -> None:
|
|
179
|
+
with _open_client() as client:
|
|
180
|
+
if args.id is None:
|
|
181
|
+
project = client.request("GET", "/projects/active").json()
|
|
182
|
+
else:
|
|
183
|
+
project = client.request("GET", f"/projects/{args.id}").json()
|
|
184
|
+
source = project.get("source") if isinstance(project, dict) else None
|
|
185
|
+
if not isinstance(source, dict):
|
|
186
|
+
source = {}
|
|
187
|
+
download_url = source.get("download_url")
|
|
188
|
+
if not download_url:
|
|
189
|
+
attachment_id = source.get("id")
|
|
190
|
+
if not attachment_id:
|
|
191
|
+
_fail("project has no map file")
|
|
192
|
+
download_url = f"/attachments/{attachment_id}/download"
|
|
193
|
+
response = client.get(str(download_url))
|
|
194
|
+
project_id = project.get("id", args.id) if isinstance(project, dict) else args.id
|
|
195
|
+
dest = _download_destination(args.output, _download_name(source, project_id))
|
|
196
|
+
dest.write_bytes(response.content)
|
|
197
|
+
print(dest)
|
|
198
|
+
|
|
199
|
+
|
|
156
200
|
_TOOL_COMMANDS = {
|
|
157
201
|
"mrmap-to-raster": "mrmap-to-raster.py",
|
|
158
202
|
"mrmap-to-bg": "mrmap-to-bg.py",
|
|
@@ -172,6 +216,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
172
216
|
help_parser = sub.add_parser("help", help="Show this help")
|
|
173
217
|
help_parser.set_defaults(func=lambda _args: parser.print_help())
|
|
174
218
|
|
|
219
|
+
version_parser = sub.add_parser("version", help="Print the package version")
|
|
220
|
+
version_parser.set_defaults(func=_cmd_version)
|
|
221
|
+
|
|
175
222
|
_add_tool(sub, "mrmap-to-raster", "Rasterize a .mrmap to PNG, GeoTIFF, and JSON")
|
|
176
223
|
_add_tool(sub, "mrmap-to-bg", "Set a .mrmap raster as a LIF layout background")
|
|
177
224
|
|
|
@@ -221,13 +268,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
221
268
|
projects_list = projects_sub.add_parser("list", help="GET /projects")
|
|
222
269
|
_add_list_flags(projects_list)
|
|
223
270
|
projects_list.set_defaults(func=_cmd_projects_list)
|
|
224
|
-
projects_create = projects_sub.add_parser(
|
|
271
|
+
projects_create = projects_sub.add_parser(
|
|
272
|
+
"create",
|
|
273
|
+
aliases=["upload"],
|
|
274
|
+
help="POST /projects with a LIF file (alias: upload)",
|
|
275
|
+
)
|
|
225
276
|
projects_create.add_argument("--lif_json", type=Path, required=True, help="Path to a .lif file")
|
|
226
277
|
projects_create.add_argument("--manufacturer", "-m", required=True)
|
|
227
278
|
projects_create.add_argument("--project_version", "--version", "-v", required=True, dest="project_version")
|
|
228
279
|
projects_create.add_argument("--description", "--desc", dest="description")
|
|
229
280
|
projects_create.add_argument("--synchronous", "--sync", action="store_true", dest="synchronous")
|
|
230
281
|
projects_create.set_defaults(func=_cmd_projects_create)
|
|
282
|
+
projects_download = projects_sub.add_parser(
|
|
283
|
+
"download",
|
|
284
|
+
help="Save a project LIF file; the active project when id is omitted",
|
|
285
|
+
)
|
|
286
|
+
projects_download.add_argument("id", type=int, nargs="?", help="Project id; omit to download the active project")
|
|
287
|
+
projects_download.add_argument("--output", "-o", type=Path, help="Output file, or an existing directory")
|
|
288
|
+
projects_download.set_defaults(func=_cmd_projects_download)
|
|
231
289
|
projects_activate = projects_sub.add_parser("activate", help="POST /projects/{id}/activate")
|
|
232
290
|
projects_activate.add_argument("id", type=int)
|
|
233
291
|
projects_activate.set_defaults(func=_cmd_projects_activate)
|
|
@@ -7,6 +7,8 @@ from awcs_scripts.api import Client, origin_of
|
|
|
7
7
|
from awcs_scripts.cli import main
|
|
8
8
|
from awcs_scripts.config import load_profile
|
|
9
9
|
|
|
10
|
+
_LIF = b'{"layouts":[]}'
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
def test_origin_strips_api_prefix():
|
|
12
14
|
assert origin_of("https://dev.wcs.automacon.ru/") == "https://dev.wcs.automacon.ru"
|
|
@@ -100,7 +102,8 @@ def test_template_execute_body(tmp_path, monkeypatch):
|
|
|
100
102
|
assert seen["body"] == {"zone": "A"}
|
|
101
103
|
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
@pytest.mark.parametrize("command", ["create", "upload"])
|
|
106
|
+
def test_projects_create_and_activate(tmp_path, monkeypatch, command):
|
|
104
107
|
_profile(tmp_path, monkeypatch)
|
|
105
108
|
lif = tmp_path / "layout.lif"
|
|
106
109
|
lif.write_text("{}", encoding="utf-8")
|
|
@@ -113,7 +116,7 @@ def test_projects_create_and_activate(tmp_path, monkeypatch):
|
|
|
113
116
|
_install(monkeypatch, handler)
|
|
114
117
|
main([
|
|
115
118
|
"projects",
|
|
116
|
-
|
|
119
|
+
command,
|
|
117
120
|
"--lif_json",
|
|
118
121
|
str(lif),
|
|
119
122
|
"-m",
|
|
@@ -143,6 +146,91 @@ def test_projects_create_and_activate(tmp_path, monkeypatch):
|
|
|
143
146
|
assert seen[1].url.path == "/api/v1/projects/12/activate"
|
|
144
147
|
|
|
145
148
|
|
|
149
|
+
def test_projects_download_active_uses_source_filename(tmp_path, monkeypatch, capsys):
|
|
150
|
+
_profile(tmp_path, monkeypatch)
|
|
151
|
+
monkeypatch.chdir(tmp_path)
|
|
152
|
+
seen = []
|
|
153
|
+
|
|
154
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
155
|
+
seen.append(request.url.path)
|
|
156
|
+
if request.url.path == "/api/v1/projects/active":
|
|
157
|
+
return httpx.Response(200, json={
|
|
158
|
+
"id": 4,
|
|
159
|
+
"source": {
|
|
160
|
+
"id": 9,
|
|
161
|
+
"filename": "nested/line.lif",
|
|
162
|
+
"download_url": "/api/v1/attachments/9/download",
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
assert request.headers["x-api-key"] == "secret-key"
|
|
166
|
+
return httpx.Response(200, content=_LIF)
|
|
167
|
+
|
|
168
|
+
_install(monkeypatch, handler)
|
|
169
|
+
main(["projects", "download"])
|
|
170
|
+
assert seen == ["/api/v1/projects/active", "/api/v1/attachments/9/download"]
|
|
171
|
+
assert (tmp_path / "line.lif").read_bytes() == _LIF
|
|
172
|
+
assert capsys.readouterr().out.strip() == "line.lif"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_projects_download_id_to_output(tmp_path, monkeypatch, capsys):
|
|
176
|
+
_profile(tmp_path, monkeypatch)
|
|
177
|
+
dest = tmp_path / "saved.lif"
|
|
178
|
+
folder = tmp_path / "maps"
|
|
179
|
+
folder.mkdir()
|
|
180
|
+
|
|
181
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
182
|
+
if request.url.path == "/api/v1/projects/12":
|
|
183
|
+
return httpx.Response(200, json={
|
|
184
|
+
"id": 12,
|
|
185
|
+
"source": {"id": 3, "filename": "layout.lif"},
|
|
186
|
+
})
|
|
187
|
+
assert request.url.path == "/api/v1/attachments/3/download"
|
|
188
|
+
return httpx.Response(200, content=_LIF)
|
|
189
|
+
|
|
190
|
+
_install(monkeypatch, handler)
|
|
191
|
+
main(["projects", "download", "12", "-o", str(dest)])
|
|
192
|
+
assert dest.read_bytes() == _LIF
|
|
193
|
+
assert capsys.readouterr().out.strip() == str(dest)
|
|
194
|
+
|
|
195
|
+
main(["projects", "download", "12", "-o", str(folder)])
|
|
196
|
+
assert (folder / "layout.lif").read_bytes() == _LIF
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_projects_download_without_source_fails(tmp_path, monkeypatch):
|
|
200
|
+
_profile(tmp_path, monkeypatch)
|
|
201
|
+
_install(monkeypatch, lambda request: httpx.Response(200, json={"id": 1, "source": None}))
|
|
202
|
+
with pytest.raises(SystemExit) as caught:
|
|
203
|
+
main(["projects", "download", "1"])
|
|
204
|
+
assert caught.value.code == 1
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_client_get_same_origin_keeps_query():
|
|
208
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
209
|
+
assert request.url.path == "/api/v1/attachments/9/download"
|
|
210
|
+
assert request.url.params["x"] == "1"
|
|
211
|
+
assert request.headers["x-api-key"] == "secret-key"
|
|
212
|
+
return httpx.Response(200, content=_LIF)
|
|
213
|
+
|
|
214
|
+
with Client("https://dev.wcs.automacon.ru", "secret-key", transport=httpx.MockTransport(handler)) as client:
|
|
215
|
+
response = client.get("https://dev.wcs.automacon.ru/api/v1/attachments/9/download?x=1")
|
|
216
|
+
assert response.content == _LIF
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_client_get_external_omits_api_key(monkeypatch):
|
|
220
|
+
seen = {}
|
|
221
|
+
|
|
222
|
+
def fake_get(url, **kwargs):
|
|
223
|
+
seen["url"] = url
|
|
224
|
+
seen["headers"] = kwargs.get("headers")
|
|
225
|
+
return httpx.Response(200, content=_LIF)
|
|
226
|
+
|
|
227
|
+
monkeypatch.setattr("awcs_scripts.api.httpx.get", fake_get)
|
|
228
|
+
with Client("https://dev.wcs.automacon.ru", "secret-key") as client:
|
|
229
|
+
response = client.get("https://files.example/map.lif")
|
|
230
|
+
assert response.content == _LIF
|
|
231
|
+
assert seen == {"url": "https://files.example/map.lif", "headers": None}
|
|
232
|
+
|
|
233
|
+
|
|
146
234
|
def _profile(tmp_path, monkeypatch):
|
|
147
235
|
config = tmp_path / "config.ini"
|
|
148
236
|
config.write_text("[default]\nurl = https://dev.wcs.automacon.ru\napikey = secret-key\n", encoding="utf-8")
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
|
|
3
5
|
from awcs_scripts.cli import main
|
|
@@ -10,6 +12,13 @@ def test_help_lists_commands(capsys):
|
|
|
10
12
|
assert "mrmap-to-bg" in text
|
|
11
13
|
assert "projects" in text
|
|
12
14
|
assert "login" in text
|
|
15
|
+
assert "version" in text
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_version(capsys):
|
|
19
|
+
main(["version"])
|
|
20
|
+
text = capsys.readouterr().out.strip()
|
|
21
|
+
assert text == importlib.metadata.version("awcs-scripts")
|
|
13
22
|
|
|
14
23
|
|
|
15
24
|
def test_help_command(capsys):
|
|
@@ -17,6 +26,7 @@ def test_help_command(capsys):
|
|
|
17
26
|
text = capsys.readouterr().out
|
|
18
27
|
assert "mission_templates" in text
|
|
19
28
|
assert "users" in text
|
|
29
|
+
assert "version" in text
|
|
20
30
|
|
|
21
31
|
|
|
22
32
|
def test_raster_help():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|