acumatica-cli 0.9.0__tar.gz → 0.9.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/PKG-INFO +1 -1
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/pyproject.toml +1 -1
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/client.py +50 -20
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/seed.py +28 -1
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/README.md +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/bootstrap.py +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/bootstrap_project.xml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/cli.py +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/config.py +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/extract.py +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/extract_manifest.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/output.py +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/run.py +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/baseline/50-gl-preferences.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/baseline/60-ledger-company.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/bootstrap/company.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/bootstrap/credit-terms.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/env +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/gitignore +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/setup/10-financial-year.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/setup/20-master-calendar.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/setup/30-open-periods.yaml +0 -0
- {acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/tenant.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "acumatica-cli"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.1"
|
|
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"
|
|
@@ -93,6 +93,9 @@ class AcumaticaClient:
|
|
|
93
93
|
self._http = httpx.Client(
|
|
94
94
|
base_url=instance.base_url, timeout=timeout, transport=transport
|
|
95
95
|
)
|
|
96
|
+
# V5/B24: first successful Company PUT this session → one re-login
|
|
97
|
+
# so later branch selectors resolve; warm re-PUT is re-login-safe
|
|
98
|
+
self._refreshed_after_company = False
|
|
96
99
|
|
|
97
100
|
def __enter__(self) -> AcumaticaClient:
|
|
98
101
|
# tenant guard (V5, docs/rest-api.md): an omitted or empty tenant is
|
|
@@ -105,27 +108,10 @@ class AcumaticaClient:
|
|
|
105
108
|
"an explicit tenant silently lands on the default tenant; "
|
|
106
109
|
"set ACU_TENANT in .env or pass --tenant"
|
|
107
110
|
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"password": self.instance.password,
|
|
111
|
-
"tenant": self.instance.tenant,
|
|
112
|
-
}
|
|
113
|
-
self._checked(self._http.post("/entity/auth/login", json=creds))
|
|
114
|
-
# tenant guard, landed side (V5, B5): login accepting the name proves
|
|
115
|
-
# nothing - a stale tenant map reroutes named logins to the default
|
|
116
|
-
# tenant, and a single-tenant instance accepts ANY name (both verified
|
|
117
|
-
# live, docs/rest-api.md). Verify where the session actually landed
|
|
118
|
-
# and refuse on mismatch; logout first, since __exit__ never runs
|
|
119
|
-
# when __enter__ raises (V6 - sessions count against the license).
|
|
111
|
+
# logout first on failure: __exit__ never runs when __enter__ raises
|
|
112
|
+
# (V6 - sessions count against the license)
|
|
120
113
|
try:
|
|
121
|
-
|
|
122
|
-
if landed.casefold() != self.instance.tenant.casefold():
|
|
123
|
-
raise RuntimeError(
|
|
124
|
-
f"tenant guard: asked for tenant {self.instance.tenant!r} "
|
|
125
|
-
f"but the session landed on {landed!r} - the instance "
|
|
126
|
-
"tenant map is stale or the tenant does not exist; check "
|
|
127
|
-
"acu tenant list and recycle the app pool"
|
|
128
|
-
)
|
|
114
|
+
self._login()
|
|
129
115
|
except BaseException:
|
|
130
116
|
self.__exit__()
|
|
131
117
|
raise
|
|
@@ -138,6 +124,50 @@ class AcumaticaClient:
|
|
|
138
124
|
finally:
|
|
139
125
|
self._http.close()
|
|
140
126
|
|
|
127
|
+
def _login(self) -> None:
|
|
128
|
+
"""POST login + refuse on landed-tenant mismatch (V5, B5).
|
|
129
|
+
|
|
130
|
+
Login accepting the name proves nothing - a stale tenant map
|
|
131
|
+
reroutes named logins to the default tenant, and a single-tenant
|
|
132
|
+
instance accepts ANY name (both verified live, docs/rest-api.md).
|
|
133
|
+
"""
|
|
134
|
+
creds: dict[str, str] = {
|
|
135
|
+
"name": self.instance.user,
|
|
136
|
+
"password": self.instance.password,
|
|
137
|
+
"tenant": self.instance.tenant,
|
|
138
|
+
}
|
|
139
|
+
self._checked(self._http.post("/entity/auth/login", json=creds))
|
|
140
|
+
landed = self._landed_tenant()
|
|
141
|
+
if landed.casefold() != self.instance.tenant.casefold():
|
|
142
|
+
raise RuntimeError(
|
|
143
|
+
f"tenant guard: asked for tenant {self.instance.tenant!r} "
|
|
144
|
+
f"but the session landed on {landed!r} - the instance "
|
|
145
|
+
"tenant map is stale or the tenant does not exist; check "
|
|
146
|
+
"acu tenant list and recycle the app pool"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def relogin(self) -> None:
|
|
150
|
+
"""Drop the cookie session and open a new one without closing transport.
|
|
151
|
+
|
|
152
|
+
Mid-session Company create leaves branch selectors empty until a
|
|
153
|
+
fresh login (V5, B24). Unlike ``__exit__``, the httpx client stays
|
|
154
|
+
open so the outer context manager still owns the session lifecycle.
|
|
155
|
+
"""
|
|
156
|
+
# empty body → Content-Length: 0 (V6 / IIS 411)
|
|
157
|
+
self._http.post("/entity/auth/logout", content=b"")
|
|
158
|
+
self._login()
|
|
159
|
+
|
|
160
|
+
def refresh_after_company(self) -> None:
|
|
161
|
+
"""Re-login once after the first successful Company PUT this session.
|
|
162
|
+
|
|
163
|
+
Warm-tenant Company re-PUT is re-login-safe (V5); further Company
|
|
164
|
+
records in the same run skip the second bounce.
|
|
165
|
+
"""
|
|
166
|
+
if self._refreshed_after_company:
|
|
167
|
+
return
|
|
168
|
+
self.relogin()
|
|
169
|
+
self._refreshed_after_company = True
|
|
170
|
+
|
|
141
171
|
def _landed_tenant(self) -> str:
|
|
142
172
|
"""The tenant this session actually landed on (login name).
|
|
143
173
|
|
|
@@ -72,6 +72,10 @@ BOOTSTRAP_ENDPOINT, BOOTSTRAP_ENTITIES = bootstrap.parse_endpoint(
|
|
|
72
72
|
# the Instance field default rather than hand-synced (V11: one spelling).
|
|
73
73
|
_DEFAULT_ENDPOINT: str = f"Default/{Instance.model_fields['api_version'].default}"
|
|
74
74
|
_SYMBOLIC_BOOTSTRAP = "bootstrap"
|
|
75
|
+
# Mid-session branch-selector failure (V5/B24): virgin-tenant apply opens
|
|
76
|
+
# the REST session pre-Company; INPreferences TransitBranchID (and kin)
|
|
77
|
+
# 500 with this message until a fresh login sees the new branch.
|
|
78
|
+
_BRANCH_EMPTY = "'Branch' cannot be empty"
|
|
75
79
|
|
|
76
80
|
|
|
77
81
|
def active_bootstrap(root: Path | None = None) -> tuple[str, frozenset[str]]:
|
|
@@ -303,6 +307,10 @@ def apply(
|
|
|
303
307
|
) -> int:
|
|
304
308
|
"""PUT every record (upsert by key); an action file invokes its action.
|
|
305
309
|
|
|
310
|
+
After the first successful Company PUT this session, re-login once so
|
|
311
|
+
branch selectors resolve for later files (V5/B24). A PUT that still
|
|
312
|
+
500s with ``'Branch' cannot be empty`` gets one re-login + retry.
|
|
313
|
+
|
|
306
314
|
Returns the record count.
|
|
307
315
|
"""
|
|
308
316
|
if isinstance(baseline, ActionFile):
|
|
@@ -315,11 +323,30 @@ def apply(
|
|
|
315
323
|
body = record
|
|
316
324
|
if any(isinstance(v, list) for v in record.values()):
|
|
317
325
|
body = _with_detail_ids(client, baseline, record)
|
|
318
|
-
client
|
|
326
|
+
_put(client, baseline.entity, body, baseline.endpoint)
|
|
327
|
+
if baseline.entity == "Company":
|
|
328
|
+
client.refresh_after_company()
|
|
319
329
|
output.data(f" PUT {baseline.entity} [{label}]")
|
|
320
330
|
return len(baseline.records)
|
|
321
331
|
|
|
322
332
|
|
|
333
|
+
def _put(
|
|
334
|
+
client: AcumaticaClient,
|
|
335
|
+
entity: str,
|
|
336
|
+
body: dict[str, Any],
|
|
337
|
+
endpoint: str | None,
|
|
338
|
+
) -> None:
|
|
339
|
+
"""PUT one record; one re-login + retry on branch-empty 500 (V5/B24)."""
|
|
340
|
+
try:
|
|
341
|
+
client.put(entity, body, endpoint=endpoint)
|
|
342
|
+
except RuntimeError as err:
|
|
343
|
+
if _BRANCH_EMPTY not in str(err):
|
|
344
|
+
raise
|
|
345
|
+
output.info(f"re-login and retry {entity} (branch empty)")
|
|
346
|
+
client.relogin()
|
|
347
|
+
client.put(entity, body, endpoint=endpoint)
|
|
348
|
+
|
|
349
|
+
|
|
323
350
|
def _with_detail_ids(
|
|
324
351
|
client: AcumaticaClient, baseline: BaselineFile, record: dict[str, Any]
|
|
325
352
|
) -> dict[str, Any]:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/baseline/10-subaccounts.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/baseline/20-accounts.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/baseline/40-ledger.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/baseline/90-uoms.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/bootstrap/company.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/bootstrap/credit-terms.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/bootstrap/features.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/setup/10-financial-year.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.9.0 → acumatica_cli-0.9.1}/src/acumatica_cli/templates/setup/30-open-periods.yaml
RENAMED
|
File without changes
|
|
File without changes
|