acumatica-cli 0.8.0__tar.gz → 0.8.2__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.
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/PKG-INFO +24 -29
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/README.md +22 -27
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/pyproject.toml +4 -4
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/bootstrap.py +1 -1
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/cli.py +27 -11
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/client.py +1 -1
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/config.py +16 -13
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/extract.py +2 -2
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/output.py +25 -2
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/run.py +3 -3
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/seed.py +1 -1
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/env +3 -2
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/tenant.py +8 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/bootstrap_project.xml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/extract_manifest.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/baseline/50-gl-preferences.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/baseline/60-ledger-company.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/bootstrap/company.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/bootstrap/credit-terms.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/gitignore +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/setup/10-financial-year.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/setup/20-master-calendar.yaml +0 -0
- {acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/setup/30-open-periods.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: acumatica-cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data
|
|
5
5
|
Author: Konstantin Borovik
|
|
6
6
|
Author-email: Konstantin Borovik <kb@lab5.ca>
|
|
@@ -10,7 +10,7 @@ Requires-Dist: pydantic>=2.7
|
|
|
10
10
|
Requires-Dist: pydantic-settings>=2.5
|
|
11
11
|
Requires-Dist: pyyaml>=6.0
|
|
12
12
|
Requires-Dist: rich>=13
|
|
13
|
-
Requires-Python: >=3.
|
|
13
|
+
Requires-Python: >=3.14
|
|
14
14
|
Project-URL: Homepage, https://lab5.ca
|
|
15
15
|
Project-URL: Repository, https://github.com/kborovik/acumatica-cli
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
@@ -21,22 +21,6 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
**No UI clicks, no Configuration Wizard.**
|
|
23
23
|
|
|
24
|
-
## How it works
|
|
25
|
-
|
|
26
|
-
1. **Extract** the target Acumatica deployment with `acu extract` — its configuration becomes YAML, config as code.
|
|
27
|
-
2. **Develop** changes in the YAML, in git: edit, review, version.
|
|
28
|
-
3. **Re-deploy** with `acu apply`, then prove the live tenant matches the code with `acu diff`.
|
|
29
|
-
|
|
30
|
-
Four commands do the work, and every one is safe to re-run:
|
|
31
|
-
|
|
32
|
-
- **`acu tenant create`** — creates a tenant and bootstraps it in one step, ready for `apply`.
|
|
33
|
-
Re-running it against an existing tenant republishes the bootstrap package instead of failing.
|
|
34
|
-
- **`acu apply`** — pushes your YAML into the tenant as keyed upserts.
|
|
35
|
-
Running it twice changes nothing.
|
|
36
|
-
- **`acu diff`** — compares your YAML against the live tenant and exits with code 2 on drift.
|
|
37
|
-
- **`acu extract`** — reads a configured tenant and writes the YAML file set back out, the inverse of `apply`.
|
|
38
|
-
Existing files are skipped unless `--force`.
|
|
39
|
-
|
|
40
24
|
> **Tested against** Acumatica ERP **26.101.0225** on Windows Server 2025,
|
|
41
25
|
> contract REST endpoint **25.200.001**. Other versions will likely work,
|
|
42
26
|
> but only this combination is verified.
|
|
@@ -75,6 +59,7 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
75
59
|
│
|
|
76
60
|
├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
|
|
77
61
|
├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
|
|
62
|
+
├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
|
|
78
63
|
├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
|
|
79
64
|
│ dump live tenant state as seed YAML (inverse of apply)
|
|
80
65
|
├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
|
|
@@ -86,25 +71,31 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
86
71
|
```
|
|
87
72
|
|
|
88
73
|
`apply` and `diff` called without FILES default to the scaffolded directories, in order: `bootstrap/`, then `baseline/`, then `setup/`.
|
|
74
|
+
`run` called without FILES defaults to `scenario/`.
|
|
89
75
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
90
76
|
Run `acu <command> --help` for details on any command.
|
|
91
77
|
|
|
92
78
|
## The data repo
|
|
93
79
|
|
|
94
|
-
Your configuration lives in its own git repo
|
|
80
|
+
Your configuration lives in its own git repo. `acu config init` scaffolds everything except `scenario/`, which you author by hand:
|
|
95
81
|
|
|
96
82
|
| Path | What it holds |
|
|
97
83
|
| ------------ | -------------------------------------------------------------------------- |
|
|
98
84
|
| `bootstrap/` | what makes a virgin tenant configurable: features, company, credit terms |
|
|
99
85
|
| `baseline/` | reference data: subaccounts, chart of accounts, ledger, units of measure |
|
|
100
86
|
| `setup/` | one-time actions: financial year, master calendar, open periods |
|
|
87
|
+
| `scenario/` | transaction scenarios for `acu run`: purchase, build, sell flows |
|
|
101
88
|
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
102
89
|
|
|
103
90
|
Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order. The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
|
|
104
91
|
|
|
92
|
+
Seed YAML in `bootstrap/`, `baseline/`, and `setup/` is state: `apply` upserts it, `diff` proves it.
|
|
93
|
+
Scenario YAML is different — it describes transactions that flow forward.
|
|
94
|
+
`acu run` executes each step in order (`put`, `action`, `wait`, `get`), captures server-assigned document numbers into `${var}` references for later steps, and checks `expect:` assertions as deltas against a pre-run snapshot, so a scenario re-runs safely on a warm tenant.
|
|
95
|
+
|
|
105
96
|
## Installation
|
|
106
97
|
|
|
107
|
-
Requires Python 3.
|
|
98
|
+
Requires Python 3.14 or newer.
|
|
108
99
|
|
|
109
100
|
```sh
|
|
110
101
|
uv tool install acumatica-cli
|
|
@@ -148,9 +139,9 @@ acu apply --dry-run # show what would be written, write nothing
|
|
|
148
139
|
`acu` talks to an instance over two independent channels:
|
|
149
140
|
|
|
150
141
|
- **Control plane (SSH):** `acu tenant` runs `ac.exe -cm:CompanyConfig` and `sqlcmd` on the Windows guest — see [`docs/ac-exe.md`](docs/ac-exe.md).
|
|
151
|
-
- **Data plane (REST):** `acu apply`, `diff`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
|
|
142
|
+
- **Data plane (REST):** `acu apply`, `diff`, `run`, `extract`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
|
|
152
143
|
|
|
153
|
-
If you
|
|
144
|
+
If you never touch `acu tenant`, you never need SSH — every other command is pure REST.
|
|
154
145
|
|
|
155
146
|
## SSH setup (control plane)
|
|
156
147
|
|
|
@@ -197,29 +188,33 @@ ssh -o BatchMode=yes Administrator@acu-dev1.vm.internal '$PSVersionTable.PSVersi
|
|
|
197
188
|
|
|
198
189
|
## Development
|
|
199
190
|
|
|
191
|
+
Requires **GNU Make at least 3.82** — the Makefile uses `.ONESHELL`.
|
|
192
|
+
On macOS use Homebrew's `gmake` (`brew install make`); `/usr/bin/make` is 3.81 and fails the guard.
|
|
193
|
+
Elsewhere plain `make` is fine when it is GNU Make.
|
|
194
|
+
|
|
200
195
|
```sh
|
|
201
196
|
git clone https://github.com/kborovik/acumatica-cli.git
|
|
202
197
|
cd acumatica-cli
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
gmake install # editable install as a global uv tool
|
|
199
|
+
gmake check # offline gate: ruff, basedpyright strict, pytest
|
|
205
200
|
```
|
|
206
201
|
|
|
207
202
|
The default test suite is fully offline.
|
|
208
203
|
REST is faked with `httpx.MockTransport`, SSH with a monkeypatched `subprocess.run` — no live instance is needed.
|
|
209
|
-
`
|
|
204
|
+
`gmake check` must pass before every commit.
|
|
210
205
|
|
|
211
206
|
### Live end-to-end tier
|
|
212
207
|
|
|
213
|
-
`
|
|
208
|
+
`gmake e2e` runs the opt-in live tier against a real Acumatica instance (pytest marker `e2e`, deselected by the default suite).
|
|
214
209
|
|
|
215
210
|
Configuration is one file: a decrypted `.env` at the repo root names the instance — `ACU_BASE_URL`, `ACU_SSH`, `ACU_TENANT`, `ACU_PASSWORD`.
|
|
216
|
-
`
|
|
211
|
+
`gmake e2e` refuses to start without it.
|
|
217
212
|
|
|
218
213
|
The tier is self-contained.
|
|
219
214
|
Each run scaffolds a synthetic single-org company from the packaged `acu config init` templates into a temporary directory, copies the real `.env` into it, and runs the installed `acu` binary from there — no data repo, no pre-existing fixtures on the instance.
|
|
220
215
|
Scratch tenants (`E2E`, `E2EA`, `E2EB`) are created on the way in and always deleted on the way out, so nothing persists.
|
|
221
216
|
|
|
222
217
|
```sh
|
|
223
|
-
|
|
224
|
-
|
|
218
|
+
gmake e2e # whole tier, about 20 minutes
|
|
219
|
+
gmake e2e FILE=test_provision_lifecycle # one file, by stem or path
|
|
225
220
|
```
|
|
@@ -4,22 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
**No UI clicks, no Configuration Wizard.**
|
|
6
6
|
|
|
7
|
-
## How it works
|
|
8
|
-
|
|
9
|
-
1. **Extract** the target Acumatica deployment with `acu extract` — its configuration becomes YAML, config as code.
|
|
10
|
-
2. **Develop** changes in the YAML, in git: edit, review, version.
|
|
11
|
-
3. **Re-deploy** with `acu apply`, then prove the live tenant matches the code with `acu diff`.
|
|
12
|
-
|
|
13
|
-
Four commands do the work, and every one is safe to re-run:
|
|
14
|
-
|
|
15
|
-
- **`acu tenant create`** — creates a tenant and bootstraps it in one step, ready for `apply`.
|
|
16
|
-
Re-running it against an existing tenant republishes the bootstrap package instead of failing.
|
|
17
|
-
- **`acu apply`** — pushes your YAML into the tenant as keyed upserts.
|
|
18
|
-
Running it twice changes nothing.
|
|
19
|
-
- **`acu diff`** — compares your YAML against the live tenant and exits with code 2 on drift.
|
|
20
|
-
- **`acu extract`** — reads a configured tenant and writes the YAML file set back out, the inverse of `apply`.
|
|
21
|
-
Existing files are skipped unless `--force`.
|
|
22
|
-
|
|
23
7
|
> **Tested against** Acumatica ERP **26.101.0225** on Windows Server 2025,
|
|
24
8
|
> contract REST endpoint **25.200.001**. Other versions will likely work,
|
|
25
9
|
> but only this combination is verified.
|
|
@@ -58,6 +42,7 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
58
42
|
│
|
|
59
43
|
├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
|
|
60
44
|
├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
|
|
45
|
+
├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
|
|
61
46
|
├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
|
|
62
47
|
│ dump live tenant state as seed YAML (inverse of apply)
|
|
63
48
|
├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
|
|
@@ -69,25 +54,31 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
69
54
|
```
|
|
70
55
|
|
|
71
56
|
`apply` and `diff` called without FILES default to the scaffolded directories, in order: `bootstrap/`, then `baseline/`, then `setup/`.
|
|
57
|
+
`run` called without FILES defaults to `scenario/`.
|
|
72
58
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
73
59
|
Run `acu <command> --help` for details on any command.
|
|
74
60
|
|
|
75
61
|
## The data repo
|
|
76
62
|
|
|
77
|
-
Your configuration lives in its own git repo
|
|
63
|
+
Your configuration lives in its own git repo. `acu config init` scaffolds everything except `scenario/`, which you author by hand:
|
|
78
64
|
|
|
79
65
|
| Path | What it holds |
|
|
80
66
|
| ------------ | -------------------------------------------------------------------------- |
|
|
81
67
|
| `bootstrap/` | what makes a virgin tenant configurable: features, company, credit terms |
|
|
82
68
|
| `baseline/` | reference data: subaccounts, chart of accounts, ledger, units of measure |
|
|
83
69
|
| `setup/` | one-time actions: financial year, master calendar, open periods |
|
|
70
|
+
| `scenario/` | transaction scenarios for `acu run`: purchase, build, sell flows |
|
|
84
71
|
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
85
72
|
|
|
86
73
|
Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order. The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
|
|
87
74
|
|
|
75
|
+
Seed YAML in `bootstrap/`, `baseline/`, and `setup/` is state: `apply` upserts it, `diff` proves it.
|
|
76
|
+
Scenario YAML is different — it describes transactions that flow forward.
|
|
77
|
+
`acu run` executes each step in order (`put`, `action`, `wait`, `get`), captures server-assigned document numbers into `${var}` references for later steps, and checks `expect:` assertions as deltas against a pre-run snapshot, so a scenario re-runs safely on a warm tenant.
|
|
78
|
+
|
|
88
79
|
## Installation
|
|
89
80
|
|
|
90
|
-
Requires Python 3.
|
|
81
|
+
Requires Python 3.14 or newer.
|
|
91
82
|
|
|
92
83
|
```sh
|
|
93
84
|
uv tool install acumatica-cli
|
|
@@ -131,9 +122,9 @@ acu apply --dry-run # show what would be written, write nothing
|
|
|
131
122
|
`acu` talks to an instance over two independent channels:
|
|
132
123
|
|
|
133
124
|
- **Control plane (SSH):** `acu tenant` runs `ac.exe -cm:CompanyConfig` and `sqlcmd` on the Windows guest — see [`docs/ac-exe.md`](docs/ac-exe.md).
|
|
134
|
-
- **Data plane (REST):** `acu apply`, `diff`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
|
|
125
|
+
- **Data plane (REST):** `acu apply`, `diff`, `run`, `extract`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
|
|
135
126
|
|
|
136
|
-
If you
|
|
127
|
+
If you never touch `acu tenant`, you never need SSH — every other command is pure REST.
|
|
137
128
|
|
|
138
129
|
## SSH setup (control plane)
|
|
139
130
|
|
|
@@ -180,29 +171,33 @@ ssh -o BatchMode=yes Administrator@acu-dev1.vm.internal '$PSVersionTable.PSVersi
|
|
|
180
171
|
|
|
181
172
|
## Development
|
|
182
173
|
|
|
174
|
+
Requires **GNU Make at least 3.82** — the Makefile uses `.ONESHELL`.
|
|
175
|
+
On macOS use Homebrew's `gmake` (`brew install make`); `/usr/bin/make` is 3.81 and fails the guard.
|
|
176
|
+
Elsewhere plain `make` is fine when it is GNU Make.
|
|
177
|
+
|
|
183
178
|
```sh
|
|
184
179
|
git clone https://github.com/kborovik/acumatica-cli.git
|
|
185
180
|
cd acumatica-cli
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
gmake install # editable install as a global uv tool
|
|
182
|
+
gmake check # offline gate: ruff, basedpyright strict, pytest
|
|
188
183
|
```
|
|
189
184
|
|
|
190
185
|
The default test suite is fully offline.
|
|
191
186
|
REST is faked with `httpx.MockTransport`, SSH with a monkeypatched `subprocess.run` — no live instance is needed.
|
|
192
|
-
`
|
|
187
|
+
`gmake check` must pass before every commit.
|
|
193
188
|
|
|
194
189
|
### Live end-to-end tier
|
|
195
190
|
|
|
196
|
-
`
|
|
191
|
+
`gmake e2e` runs the opt-in live tier against a real Acumatica instance (pytest marker `e2e`, deselected by the default suite).
|
|
197
192
|
|
|
198
193
|
Configuration is one file: a decrypted `.env` at the repo root names the instance — `ACU_BASE_URL`, `ACU_SSH`, `ACU_TENANT`, `ACU_PASSWORD`.
|
|
199
|
-
`
|
|
194
|
+
`gmake e2e` refuses to start without it.
|
|
200
195
|
|
|
201
196
|
The tier is self-contained.
|
|
202
197
|
Each run scaffolds a synthetic single-org company from the packaged `acu config init` templates into a temporary directory, copies the real `.env` into it, and runs the installed `acu` binary from there — no data repo, no pre-existing fixtures on the instance.
|
|
203
198
|
Scratch tenants (`E2E`, `E2EA`, `E2EB`) are created on the way in and always deleted on the way out, so nothing persists.
|
|
204
199
|
|
|
205
200
|
```sh
|
|
206
|
-
|
|
207
|
-
|
|
201
|
+
gmake e2e # whole tier, about 20 minutes
|
|
202
|
+
gmake e2e FILE=test_provision_lifecycle # one file, by stem or path
|
|
208
203
|
```
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "acumatica-cli"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.2"
|
|
4
4
|
description = "Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data"
|
|
5
5
|
authors = [{ name = "Konstantin Borovik", email = "kb@lab5.ca" }]
|
|
6
6
|
readme = "README.md"
|
|
7
|
-
requires-python = ">=3.
|
|
7
|
+
requires-python = ">=3.14"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"click>=8.1",
|
|
10
10
|
"httpx>=0.27",
|
|
@@ -41,7 +41,7 @@ markers = [
|
|
|
41
41
|
|
|
42
42
|
[tool.basedpyright]
|
|
43
43
|
typeCheckingMode = "strict"
|
|
44
|
-
pythonVersion = "3.
|
|
44
|
+
pythonVersion = "3.14"
|
|
45
45
|
include = ["src/acumatica_cli/", "tests/"]
|
|
46
46
|
reportMissingTypeStubs = false
|
|
47
47
|
reportUnknownVariableType = false
|
|
@@ -52,7 +52,7 @@ reportUnknownLambdaType = false
|
|
|
52
52
|
reportUnusedFunction = "warning"
|
|
53
53
|
|
|
54
54
|
[tool.ruff]
|
|
55
|
-
target-version = "
|
|
55
|
+
target-version = "py314"
|
|
56
56
|
src = ["src"]
|
|
57
57
|
|
|
58
58
|
[tool.ruff.format]
|
|
@@ -150,7 +150,7 @@ def _published_description(client: AcumaticaClient) -> str | None:
|
|
|
150
150
|
try:
|
|
151
151
|
with zipfile.ZipFile(io.BytesIO(content)) as zf:
|
|
152
152
|
root = ET.fromstring(zf.read("project.xml"))
|
|
153
|
-
except
|
|
153
|
+
except zipfile.BadZipFile, KeyError, ET.ParseError:
|
|
154
154
|
return None
|
|
155
155
|
return root.get("description")
|
|
156
156
|
|
|
@@ -379,12 +379,13 @@ def config_check(ctx: click.Context) -> None:
|
|
|
379
379
|
|
|
380
380
|
Dependency order: discovery (.env walk-up + parse + ACU_BASE_URL), then
|
|
381
381
|
secrets (ACU_PASSWORD resolved), then REST (login, landed-tenant verify,
|
|
382
|
-
logout) and ssh
|
|
383
|
-
|
|
382
|
+
logout) and ssh probed independently - ssh set → trivial remote; ssh
|
|
383
|
+
unset → skip (ACU_SSH optional, V3 hosted path), never fail. A discovery
|
|
384
|
+
or secrets failure stops; a REST failure still probes ssh when set and
|
|
384
385
|
vice versa. Discovery is lax (V3): no .env passes when --url covers
|
|
385
386
|
base_url, and flags-only runs (no .env anywhere) are valid. Writes
|
|
386
|
-
nothing: no PUTs, no tenant CRUD. Exit 0 when every probe
|
|
387
|
-
any failure.
|
|
387
|
+
nothing: no PUTs, no tenant CRUD. Exit 0 when every non-skipped probe
|
|
388
|
+
passes, 1 on any failure.
|
|
388
389
|
"""
|
|
389
390
|
overrides: dict[str, str] = ctx.obj or {}
|
|
390
391
|
# discovery (V3): lax walk-up + parse; base_url (the primary identity
|
|
@@ -432,12 +433,16 @@ def config_check(ctx: click.Context) -> None:
|
|
|
432
433
|
except (RuntimeError, httpx.HTTPError) as exc:
|
|
433
434
|
output.data(f"fail rest: {exc}")
|
|
434
435
|
failed = True
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
output.data(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
436
|
+
if not inst.ssh:
|
|
437
|
+
# V3/I.cmd: ACU_SSH optional — hosted data-plane path skips, never fails
|
|
438
|
+
output.data("skip ssh (ACU_SSH not set)")
|
|
439
|
+
else:
|
|
440
|
+
try:
|
|
441
|
+
TenantManager(inst).ping()
|
|
442
|
+
output.data(f"ok ssh ({inst.ssh})")
|
|
443
|
+
except RuntimeError as exc:
|
|
444
|
+
output.data(f"fail ssh: {exc}")
|
|
445
|
+
failed = True
|
|
441
446
|
if failed:
|
|
442
447
|
raise SystemExit(1)
|
|
443
448
|
|
|
@@ -553,7 +558,18 @@ def diff_cmd(inst: Instance, files: tuple[Path, ...]) -> None:
|
|
|
553
558
|
with AcumaticaClient(inst) as client:
|
|
554
559
|
for path in paths:
|
|
555
560
|
baseline = seed.load_baseline(path)
|
|
556
|
-
|
|
561
|
+
# same file banner as apply so a long multi-file diff shows
|
|
562
|
+
# progress instead of silence until the final status line
|
|
563
|
+
output.data(
|
|
564
|
+
f"{path} -> {inst.tenant} on {inst.base_url} ({baseline.entity})"
|
|
565
|
+
)
|
|
566
|
+
file_drifts = seed.diff(client, baseline)
|
|
567
|
+
drifts += file_drifts
|
|
568
|
+
n = 1 if isinstance(baseline, seed.ActionFile) else len(baseline.records)
|
|
569
|
+
if file_drifts:
|
|
570
|
+
output.data(f" {n} record(s), {len(file_drifts)} drift(s)")
|
|
571
|
+
else:
|
|
572
|
+
output.data(f" {n} record(s) ok")
|
|
557
573
|
_exit_on_drift(inst, drifts, len(paths))
|
|
558
574
|
|
|
559
575
|
|
|
@@ -94,7 +94,7 @@ class AcumaticaClient:
|
|
|
94
94
|
base_url=instance.base_url, timeout=timeout, transport=transport
|
|
95
95
|
)
|
|
96
96
|
|
|
97
|
-
def __enter__(self) ->
|
|
97
|
+
def __enter__(self) -> AcumaticaClient:
|
|
98
98
|
# tenant guard (V5, docs/rest-api.md): an omitted or empty tenant is
|
|
99
99
|
# the one login the server still routes silently — to the default
|
|
100
100
|
# tenant. Defense-in-depth vs wrong-tenant writes: every data-plane
|
|
@@ -5,9 +5,10 @@ env prefix ``ACU_``, and the sole config file is ``.env`` (found by walking
|
|
|
5
5
|
up from cwd) carrying where + secrets as ``ACU_*`` vars. The file is
|
|
6
6
|
optional - flags plus the process environment can supply the full config.
|
|
7
7
|
Per key the first set value wins: flag, ``ACU_*`` var (process environment
|
|
8
|
-
over a found ``.env``), code default. ``base_url``
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
over a found ``.env``), code default. ``base_url`` is the only required
|
|
9
|
+
address (REST data plane); ``ssh`` is optional control-plane address
|
|
10
|
+
(empty = data-plane only; tenant cmds hard-error when unresolved — V1/V3).
|
|
11
|
+
The password must resolve via ``--password`` or ``ACU_PASSWORD``.
|
|
11
12
|
"""
|
|
12
13
|
|
|
13
14
|
from collections.abc import Iterator, Mapping
|
|
@@ -51,11 +52,12 @@ INIT_TEMPLATES = (
|
|
|
51
52
|
class Instance(BaseSettings):
|
|
52
53
|
"""The resolved target: flags over ACU_* vars (.env or process) over defaults.
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
``user@host
|
|
57
|
-
Unknown ``ACU_*`` vars are ignored,
|
|
58
|
-
``.env`` legitimately carry
|
|
55
|
+
Explicit addresses, no derivation (V1): ``base_url`` is the REST root
|
|
56
|
+
(scheme + host + site path); ``ssh`` is the optional control-plane
|
|
57
|
+
``user@host`` (empty = data-plane only). Install-layout values are
|
|
58
|
+
module constants, not fields. Unknown ``ACU_*`` vars are ignored,
|
|
59
|
+
never errors - the environment and ``.env`` legitimately carry
|
|
60
|
+
non-config vars (``ACU_DEBUG``).
|
|
59
61
|
"""
|
|
60
62
|
|
|
61
63
|
model_config = SettingsConfigDict(
|
|
@@ -65,7 +67,7 @@ class Instance(BaseSettings):
|
|
|
65
67
|
)
|
|
66
68
|
|
|
67
69
|
base_url: str # REST root: scheme + host + site path
|
|
68
|
-
ssh: str # control plane: full user@host
|
|
70
|
+
ssh: str = "" # control plane: full user@host; empty = data-plane only
|
|
69
71
|
tenant: str = ""
|
|
70
72
|
api_version: str = "25.200.001" # V11: /entity/Default/<api_version>/
|
|
71
73
|
user: str = "admin" # ACU_USER; the --username flag maps here
|
|
@@ -76,8 +78,8 @@ class Instance(BaseSettings):
|
|
|
76
78
|
@model_validator(mode="before")
|
|
77
79
|
@classmethod
|
|
78
80
|
def _blank_required_is_unset(cls, data: Any) -> Any:
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
+
# blank ACU_BASE_URL= / ACU_SSH= reads as unset (V3): base_url then
|
|
82
|
+
# fails required; ssh falls through to the empty default (optional)
|
|
81
83
|
if isinstance(data, dict):
|
|
82
84
|
for key in ("base_url", "ssh"):
|
|
83
85
|
if data.get(key) == "":
|
|
@@ -161,8 +163,9 @@ def load_instance(overrides: Mapping[str, str | None] | None = None) -> Instance
|
|
|
161
163
|
``overrides`` carries the global flags keyed by Instance field name;
|
|
162
164
|
per key the first set value wins (flag, ACU_* var - process environment
|
|
163
165
|
over a found .env - code default). No .env is fine (V3): the hard error
|
|
164
|
-
comes only when a required value (base_url,
|
|
165
|
-
unresolved after the merge, naming the missing key.
|
|
166
|
+
comes only when a required value (base_url, password) is still
|
|
167
|
+
unresolved after the merge, naming the missing key. ``ssh`` is optional
|
|
168
|
+
(hosted / data-plane-only path); tenant cmds hard-error when it is empty.
|
|
166
169
|
"""
|
|
167
170
|
flags = {k: v for k, v in dict(overrides or {}).items() if v is not None}
|
|
168
171
|
root = find_data_root()
|
|
@@ -66,7 +66,7 @@ class EntitySpec(Model):
|
|
|
66
66
|
return BOOTSTRAP_ENDPOINT if v == "bootstrap" else v
|
|
67
67
|
|
|
68
68
|
@model_validator(mode="after")
|
|
69
|
-
def _strip_include_exclusive(self) ->
|
|
69
|
+
def _strip_include_exclusive(self) -> EntitySpec:
|
|
70
70
|
if self.strip and self.include:
|
|
71
71
|
raise ValueError("strip and include are mutually exclusive")
|
|
72
72
|
return self
|
|
@@ -96,7 +96,7 @@ class Manifest(Model):
|
|
|
96
96
|
setup: list[SetupSynth] = Field(default_factory=list)
|
|
97
97
|
|
|
98
98
|
@model_validator(mode="after")
|
|
99
|
-
def _self_consistent(self) ->
|
|
99
|
+
def _self_consistent(self) -> Manifest:
|
|
100
100
|
# V20 by construction: an emitted file for an entity both endpoints
|
|
101
101
|
# serve must carry an endpoint, or load_baseline rejects it
|
|
102
102
|
for spec in self.entities:
|
|
@@ -6,6 +6,7 @@ and spinners on a TTY and degrades to plain deterministic text when piped
|
|
|
6
6
|
stderr carries status, warnings, and errors.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import os
|
|
9
10
|
from collections.abc import Generator, Iterable
|
|
10
11
|
from contextlib import contextmanager
|
|
11
12
|
|
|
@@ -13,8 +14,30 @@ from rich import box
|
|
|
13
14
|
from rich.console import Console
|
|
14
15
|
from rich.table import Table
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
def _force_terminal() -> bool | None:
|
|
19
|
+
"""Map NO_COLOR / FORCE_COLOR to rich's force_terminal flag.
|
|
20
|
+
|
|
21
|
+
None leaves auto-detect (isatty). False forces plain/piped rendering.
|
|
22
|
+
True forces TTY chrome even when piped.
|
|
23
|
+
|
|
24
|
+
Rich treats any non-empty FORCE_COLOR as "on", so FORCE_COLOR=0 would
|
|
25
|
+
otherwise enable color. We treat empty and "0" as off (force-color.org).
|
|
26
|
+
NO_COLOR (any value) always wins and forces plain output (no-color.org).
|
|
27
|
+
"""
|
|
28
|
+
if "NO_COLOR" in os.environ:
|
|
29
|
+
return False
|
|
30
|
+
force = os.environ.get("FORCE_COLOR")
|
|
31
|
+
if force is not None:
|
|
32
|
+
return force not in ("", "0")
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_force = _force_terminal()
|
|
37
|
+
out = Console(markup=False, emoji=False, highlight=False, force_terminal=_force)
|
|
38
|
+
err = Console(
|
|
39
|
+
stderr=True, markup=False, emoji=False, highlight=False, force_terminal=_force
|
|
40
|
+
)
|
|
18
41
|
|
|
19
42
|
|
|
20
43
|
def data(msg: str) -> None:
|
|
@@ -99,7 +99,7 @@ class Step(Model):
|
|
|
99
99
|
endpoint: str | None = None
|
|
100
100
|
|
|
101
101
|
@model_validator(mode="after")
|
|
102
|
-
def _one_op(self) ->
|
|
102
|
+
def _one_op(self) -> Step:
|
|
103
103
|
ops = [op for op in (self.put, self.action, self.get) if op is not None]
|
|
104
104
|
if len(ops) > 1:
|
|
105
105
|
raise ValueError(f"step '{self.id}': put, action, get are exclusive")
|
|
@@ -134,7 +134,7 @@ class Expect(Model):
|
|
|
134
134
|
endpoint: str | None = None
|
|
135
135
|
|
|
136
136
|
@model_validator(mode="after")
|
|
137
|
-
def _one_kind(self) ->
|
|
137
|
+
def _one_kind(self) -> Expect:
|
|
138
138
|
if (self.get is None) == (self.inquire is None):
|
|
139
139
|
raise ValueError("expect: exactly one of get, inquire")
|
|
140
140
|
if self.get is not None and self.fields is None:
|
|
@@ -164,7 +164,7 @@ class Scenario(Model):
|
|
|
164
164
|
expect: list[Expect] = Field(default_factory=list)
|
|
165
165
|
|
|
166
166
|
@model_validator(mode="after")
|
|
167
|
-
def _unique_step_ids(self) ->
|
|
167
|
+
def _unique_step_ids(self) -> Scenario:
|
|
168
168
|
seen: set[str] = set()
|
|
169
169
|
for step in self.steps:
|
|
170
170
|
if step.id in seen:
|
|
@@ -109,7 +109,7 @@ class BaselineFile(Model):
|
|
|
109
109
|
return v if isinstance(v, list) else [v]
|
|
110
110
|
|
|
111
111
|
@model_validator(mode="after")
|
|
112
|
-
def _keys_identify_records(self) ->
|
|
112
|
+
def _keys_identify_records(self) -> BaselineFile:
|
|
113
113
|
# V25: the declared key tuple must uniquely identify records - a
|
|
114
114
|
# dup-keyed file diffs as permanent false drift and apply collapses
|
|
115
115
|
# the dup records into one PUT target (B21)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# scaffolded by `acu config init` - the sole config file: where + secrets,
|
|
2
2
|
# every key an ACU_* var; print the fully resolved document with:
|
|
3
3
|
# acu config show
|
|
4
|
-
# ACU_BASE_URL (REST root)
|
|
5
|
-
#
|
|
4
|
+
# ACU_BASE_URL (REST root) is required; ACU_SSH (control-plane user@host)
|
|
5
|
+
# is optional - leave blank for hosted / data-plane-only (tenant CRUD still
|
|
6
|
+
# needs it). The rest are code defaults you can override here.
|
|
6
7
|
# Keep this file out of git (the scaffolded .gitignore already lists it;
|
|
7
8
|
# encrypt at rest, e.g. .env.gpg).
|
|
8
9
|
ACU_BASE_URL=http://erp.example.com/AcumaticaERP
|
|
@@ -28,6 +28,14 @@ class TenantManager:
|
|
|
28
28
|
"""Tenant CRUD via SSH to the Windows guest."""
|
|
29
29
|
|
|
30
30
|
def __init__(self, instance: Instance):
|
|
31
|
+
# control-plane address is optional on Instance (hosted path, V3);
|
|
32
|
+
# tenant CRUD still needs it - hard-error naming the key before any
|
|
33
|
+
# remote (I.cmd), never an empty ssh argv
|
|
34
|
+
if not instance.ssh:
|
|
35
|
+
raise SystemExit(
|
|
36
|
+
"ACU_SSH not set (pass --ssh, "
|
|
37
|
+
"or put ACU_SSH in .env or the environment)"
|
|
38
|
+
)
|
|
31
39
|
self.instance = instance
|
|
32
40
|
|
|
33
41
|
def _ssh(self, command: str) -> str:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/baseline/10-subaccounts.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/baseline/20-accounts.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/baseline/40-ledger.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/baseline/90-uoms.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/bootstrap/company.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/bootstrap/credit-terms.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/bootstrap/features.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/setup/10-financial-year.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.8.0 → acumatica_cli-0.8.2}/src/acumatica_cli/templates/setup/30-open-periods.yaml
RENAMED
|
File without changes
|