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.
@@ -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.1
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
- ## Публикация в PyPI
20
+ ## Использование
21
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
- ## Установка
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
- uv sync
60
- uv run awcs help
61
- uv build
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
- Снимает команду `awcs`, поставленную через `uv tool install` из клона или с PyPI:
52
+ #### Основное
67
53
 
68
54
  ```
69
- uv tool uninstall awcs-scripts
55
+ awcs help
56
+ awcs version
70
57
  ```
71
58
 
72
- Профиль с адресом и ключом эта команда не трогает. Его файл лежит по `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Чтобы убрать и ключ, удалите этот файл.
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
- 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
96
+ uv tool uninstall awcs-scripts
99
97
  ```
100
98
 
101
- `awcs mrmap-to-raster` пишет PNG, GeoTIFF и JSON. `--resolution` задаёт метры на пиксель, по умолчанию берётся `resolution_2d` из карты. `awcs mrmap-to-bg` подставляет этот растр в `backgroundImage` одного layout. Без `--output` исходный `.lif` перезаписывается.
99
+ Профиль с адресом и ключом эта команда не трогает. Его файл лежит по `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Чтобы убрать и ключ, удалите этот файл.
102
100
 
103
- `awcs login` проверяет ключ через `GET /users/me` и только после ответа 200 сохраняет origin и ключ. Профиль лежит вне репозитория: путь из `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Ключ в вывод не печатается. Клиент сам дописывает `/api/v1` и шлёт заголовок `X-Api-Key`.
101
+ ## Разработка
104
102
 
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`.
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
- ## Публикация в PyPI
5
+ ## Использование
6
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
- ## Установка
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
- uv sync
45
- uv run awcs help
46
- uv build
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
- Снимает команду `awcs`, поставленную через `uv tool install` из клона или с PyPI:
37
+ #### Основное
52
38
 
53
39
  ```
54
- uv tool uninstall awcs-scripts
40
+ awcs help
41
+ awcs version
55
42
  ```
56
43
 
57
- Профиль с адресом и ключом эта команда не трогает. Его файл лежит по `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Чтобы убрать и ключ, удалите этот файл.
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
- 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
81
+ uv tool uninstall awcs-scripts
84
82
  ```
85
83
 
86
- `awcs mrmap-to-raster` пишет PNG, GeoTIFF и JSON. `--resolution` задаёт метры на пиксель, по умолчанию берётся `resolution_2d` из карты. `awcs mrmap-to-bg` подставляет этот растр в `backgroundImage` одного layout. Без `--output` исходный `.lif` перезаписывается.
84
+ Профиль с адресом и ключом эта команда не трогает. Его файл лежит по `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Чтобы убрать и ключ, удалите этот файл.
87
85
 
88
- `awcs login` проверяет ключ через `GET /users/me` и только после ответа 200 сохраняет origin и ключ. Профиль лежит вне репозитория: путь из `AWCS_CONFIG`, иначе `%APPDATA%\awcs\config.ini` на Windows и `~/.config/awcs/config.ini` в остальных системах. Ключ в вывод не печатается. Клиент сам дописывает `/api/v1` и шлёт заголовок `X-Api-Key`.
86
+ ## Разработка
89
87
 
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`.
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`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "awcs-scripts"
3
- version = "1.0.1"
3
+ version = "1.1.0"
4
4
  description = "Command line scripts for WCS maps and the ARMS API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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()
@@ -1,3 +1,3 @@
1
1
  """Scripts for WCS maps and the ARMS API."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "1.1.0"
@@ -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("create", help="POST /projects with a LIF file")
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
- def test_projects_create_and_activate(tmp_path, monkeypatch):
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
- "create",
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():
@@ -23,7 +23,7 @@ wheels = [
23
23
 
24
24
  [[package]]
25
25
  name = "awcs-scripts"
26
- version = "1.0.1"
26
+ version = "1.1.0"
27
27
  source = { editable = "." }
28
28
  dependencies = [
29
29
  { name = "httpx" },
File without changes
File without changes