acumatica-cli 0.12.0__tar.gz → 0.12.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.
Files changed (70) hide show
  1. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/PKG-INFO +1 -1
  2. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/pyproject.toml +1 -1
  3. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/cli.py +89 -33
  4. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/client.py +55 -9
  5. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/README.md +0 -0
  6. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/__init__.py +0 -0
  7. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/bootstrap.py +0 -0
  8. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
  9. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/bootstrap_project.xml +0 -0
  10. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/config.py +0 -0
  11. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/extract.py +0 -0
  12. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/extract_manifest.yaml +0 -0
  13. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/firstlogin.py +0 -0
  14. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/models.py +0 -0
  15. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/output.py +0 -0
  16. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/run.py +0 -0
  17. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/seed.py +0 -0
  18. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/target.py +0 -0
  19. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/README.md +0 -0
  20. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/baseline/20-accounts.yaml +0 -0
  21. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/baseline/60-ledger-company.yaml +0 -0
  22. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/baseline/90-uoms.yaml +0 -0
  23. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/bootstrap/company.yaml +0 -0
  24. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/bootstrap/features.yaml +0 -0
  25. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/10-reason-codes.yaml +0 -0
  26. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/20-in-preferences.yaml +0 -0
  27. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/30-availability-rules.yaml +0 -0
  28. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/40-posting-classes.yaml +0 -0
  29. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/50-warehouse.yaml +0 -0
  30. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/51-warehouse-locations.yaml +0 -0
  31. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/52-warehouse-defaults.yaml +0 -0
  32. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/53-tax-categories.yaml +0 -0
  33. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/54-item-classes.yaml +0 -0
  34. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/56-so-preferences.yaml +0 -0
  35. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/57-po-preferences.yaml +0 -0
  36. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/58-order-types.yaml +0 -0
  37. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/60-ar-preferences.yaml +0 -0
  38. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/61-ap-preferences.yaml +0 -0
  39. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/62-ca-preferences.yaml +0 -0
  40. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/63-cash-account.yaml +0 -0
  41. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/64-payment-methods.yaml +0 -0
  42. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/65-statement-cycles.yaml +0 -0
  43. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/70-vendor-classes.yaml +0 -0
  44. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/71-customer-classes.yaml +0 -0
  45. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/75-vendors.yaml +0 -0
  46. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/76-customers.yaml +0 -0
  47. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/80-stock-items-parts.yaml +0 -0
  48. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/82-stock-items-kits.yaml +0 -0
  49. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/master/85-kit-specifications.yaml +0 -0
  50. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/scenario/10-seed-capital.yaml +0 -0
  51. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/scenario/20-buy-gateways.yaml +0 -0
  52. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/scenario/30-build.yaml +0 -0
  53. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/scenario/40-sell.yaml +0 -0
  54. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/distribution/setup/30-open-periods.yaml +0 -0
  55. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/baseline/10-subaccounts.yaml +0 -0
  56. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/baseline/20-accounts.yaml +0 -0
  57. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/baseline/40-ledger.yaml +0 -0
  58. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/baseline/50-gl-preferences.yaml +0 -0
  59. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/baseline/60-ledger-company.yaml +0 -0
  60. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/baseline/90-uoms.yaml +0 -0
  61. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/bootstrap/company.yaml +0 -0
  62. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/bootstrap/credit-terms.yaml +0 -0
  63. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/bootstrap/features.yaml +0 -0
  64. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/env +0 -0
  65. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/gitignore +0 -0
  66. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/setup/10-financial-year.yaml +0 -0
  67. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/setup/20-master-calendar.yaml +0 -0
  68. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/setup/30-open-periods.yaml +0 -0
  69. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/templates/finance/target +0 -0
  70. {acumatica_cli-0.12.0 → acumatica_cli-0.12.1}/src/acumatica_cli/tenant.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acumatica-cli
3
- Version: 0.12.0
3
+ Version: 0.12.1
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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acumatica-cli"
3
- version = "0.12.0"
3
+ version = "0.12.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
  license = "PolyForm-Noncommercial-1.0.0"
@@ -537,37 +537,73 @@ def config_check(ctx: click.Context, strict: bool) -> None:
537
537
  # both live probes run through the exact objects live commands use, so
538
538
  # a pass here proves the real code path, not a parallel one
539
539
  inst = _resolve_instance(ctx)
540
- failed = _probe_target(root, inst, strict=strict)
540
+ failed, claimed_erp = _probe_target(root, inst, strict=strict)
541
+ if not _probe_rest(inst, claimed_erp):
542
+ failed = True
543
+ if not _probe_ssh(inst):
544
+ failed = True
545
+ if failed:
546
+ raise SystemExit(1)
547
+
548
+
549
+ def _probe_rest(inst: Instance, claimed_erp: str | None) -> bool:
550
+ """REST login + endpoints + optional ERP build; True = all passed.
551
+
552
+ Entering the client is the probe: login + landed-tenant verify (V5);
553
+ context manager guarantees logout (V6). One GET /entity feeds endpoints
554
+ (T74/T90) and optional ERP build (T92).
555
+ """
541
556
  try:
542
- # entering the client is the whole probe: login + landed-tenant
543
- # verify (V5), and the context manager guarantees logout (V6);
544
- # endpoints probe (T74) reuses the same session when login succeeds
545
557
  with AcumaticaClient(inst) as client:
546
558
  output.data(f"ok rest ({inst.base_url}, tenant {inst.tenant})")
547
- if not _probe_endpoints(client, inst):
548
- failed = True
559
+ return _probe_entity_root(client, inst, claimed_erp)
549
560
  except (RuntimeError, httpx.HTTPError) as exc:
550
561
  output.data(f"fail rest: {exc}")
551
- failed = True
562
+ return False
563
+
564
+
565
+ def _probe_entity_root(
566
+ client: AcumaticaClient, inst: Instance, claimed_erp: str | None
567
+ ) -> bool:
568
+ """Endpoints (+ ERP when claimed); True = all passed."""
569
+ ok = True
570
+ try:
571
+ endpoints, live_build = client.entity_root()
572
+ except (RuntimeError, httpx.HTTPError) as exc:
573
+ output.data(f"fail endpoints: {exc}")
574
+ endpoints, live_build = [], None
575
+ ok = False
576
+ else:
577
+ if not _probe_endpoints(endpoints, inst):
578
+ ok = False
579
+ if claimed_erp is not None and not _probe_erp(claimed_erp, live_build):
580
+ ok = False
581
+ return ok
582
+
583
+
584
+ def _probe_ssh(inst: Instance) -> bool:
585
+ """SSH ping or skip when unset; True = pass/skip, False = fail."""
552
586
  if not inst.ssh:
553
587
  # V3/I.cmd: ACU_SSH optional — hosted data-plane path skips, never fails
554
588
  output.data("skip ssh (ACU_SSH not set)")
555
- else:
556
- try:
557
- TenantManager(inst).ping()
558
- output.data(f"ok ssh ({inst.ssh})")
559
- except RuntimeError as exc:
560
- output.data(f"fail ssh: {exc}")
561
- failed = True
562
- if failed:
563
- raise SystemExit(1)
589
+ return True
590
+ try:
591
+ TenantManager(inst).ping()
592
+ output.data(f"ok ssh ({inst.ssh})")
593
+ return True
594
+ except RuntimeError as exc:
595
+ output.data(f"fail ssh: {exc}")
596
+ return False
564
597
 
565
598
 
566
- def _probe_target(root: Path | None, inst: Instance, *, strict: bool) -> bool:
567
- """Emit the local target probe line; return True when it failed (V27).
599
+ def _probe_target(
600
+ root: Path | None, inst: Instance, *, strict: bool
601
+ ) -> tuple[bool, str | None]:
602
+ """Emit local target probe; return ``(failed, claimed_erp?)`` (V27).
568
603
 
569
604
  Invalid target hard-exits (any loader). Missing under data root warns
570
- unless --strict. No data root → skip. Match → ok (erp claimed-only).
605
+ unless --strict. No data root → skip. Match → ok + claimed ``erp`` for
606
+ the live ERP probe after REST (T92).
571
607
  """
572
608
  try:
573
609
  target = load_target(root)
@@ -576,7 +612,7 @@ def _probe_target(root: Path | None, inst: Instance, *, strict: bool) -> bool:
576
612
  raise SystemExit(1) from exc
577
613
  if root is None:
578
614
  output.data("skip target (no data root)")
579
- return False
615
+ return False, None
580
616
  if target is None:
581
617
  msg = (
582
618
  f"target: no target.yaml under {root} - dataset verified matrix "
@@ -584,35 +620,27 @@ def _probe_target(root: Path | None, inst: Instance, *, strict: bool) -> bool:
584
620
  "to require it"
585
621
  )
586
622
  output.data(f"{'fail' if strict else 'warn'} {msg}")
587
- return strict
623
+ return strict, None
588
624
  if target.default_api != inst.api_version:
589
625
  output.data(
590
626
  f"fail target: dataset default_api={target.default_api} vs "
591
627
  f"configured {inst.api_version}"
592
628
  )
593
- return True
629
+ return True, None
594
630
  output.data(
595
631
  f"ok target (default_api={target.default_api} matches configured; "
596
632
  f"erp={target.erp} claimed)"
597
633
  )
598
- # T76: no stable HTTP ERP-build discovery (V12) — keep erp claimed-only;
599
- # skip rather than invent SSH/sqlcmd (control-plane exclusion, V1)
600
- output.data(f"skip erp (live probe not available; claimed {target.erp})")
601
- return False
634
+ return False, target.erp
602
635
 
603
636
 
604
- def _probe_endpoints(client: AcumaticaClient, inst: Instance) -> bool:
637
+ def _probe_endpoints(endpoints: list[tuple[str, str]], inst: Instance) -> bool:
605
638
  """Emit endpoints probe; return True on pass, False on fail (V12/V27).
606
639
 
607
640
  Exact match: a Default entry whose version half equals
608
- ``Instance.api_version``. Fail-closed when GET /entity is unparseable.
641
+ ``Instance.api_version``. Caller already fail-closed on unparseable GET.
609
642
  """
610
643
  want = f"Default/{inst.api_version}"
611
- try:
612
- endpoints = client.list_endpoints()
613
- except (RuntimeError, httpx.HTTPError) as exc:
614
- output.data(f"fail endpoints: {exc}")
615
- return False
616
644
  defaults = [v for name, v in endpoints if name == "Default"]
617
645
  if inst.api_version in defaults:
618
646
  output.data(f"ok endpoints ({want} present)")
@@ -625,6 +653,34 @@ def _probe_endpoints(client: AcumaticaClient, inst: Instance) -> bool:
625
653
  return False
626
654
 
627
655
 
656
+ def _major_minor(version: str) -> str:
657
+ """First two dotted segments (T76/T92 major.minor match)."""
658
+ parts = version.split(".")
659
+ if len(parts) >= 2:
660
+ return f"{parts[0]}.{parts[1]}"
661
+ return version
662
+
663
+
664
+ def _probe_erp(claimed: str, live: str | None) -> bool:
665
+ """Emit ERP probe; return True on pass/skip, False on fail (T92).
666
+
667
+ Live id comes from 26.x ``GET /entity`` wrapper
668
+ ``version.acumaticaBuildVersion``. Bare array → skip (no HTTP surface).
669
+ Compare major.minor only — patch builds may drift within a claimed line.
670
+ """
671
+ if live is None:
672
+ output.data(f"skip erp (live probe not available; claimed {claimed})")
673
+ return True
674
+ if _major_minor(live) == _major_minor(claimed):
675
+ output.data(f"ok erp ({live} matches claimed {claimed})")
676
+ return True
677
+ output.data(
678
+ f"fail erp: live {live} vs claimed {claimed} "
679
+ f"(major.minor {_major_minor(live)} vs {_major_minor(claimed)})"
680
+ )
681
+ return False
682
+
683
+
628
684
  SEED_DIRS = ("bootstrap", "baseline", "setup", "master")
629
685
 
630
686
 
@@ -65,10 +65,22 @@ def unwrap(entity: dict[str, Any]) -> dict[str, Any]:
65
65
  def parse_entity_list(response: httpx.Response) -> list[tuple[str, str]]:
66
66
  """Parse ``GET /entity`` into ``[(name, version), ...]`` (fail-closed).
67
67
 
68
- Vendor contract shape (Acumatica help + docs/rest-api.md): a JSON array
69
- of objects with ``name`` and ``version`` strings. Unparseable body →
70
- RuntimeError with status, content-type, and a short raw hint so a
71
- shape change is re-verified (V12) rather than silently skipped.
68
+ Dual shape (V31 / gh #20): top-level JSON array (legacy) or object with
69
+ an ``endpoints`` array (26.x wrapper). Each row needs string ``name``
70
+ and ``version``. Unparseable body → RuntimeError with status,
71
+ content-type, and a short raw hint so a shape change is re-verified
72
+ (V12) rather than silently skipped.
73
+ """
74
+ return parse_entity_response(response)[0]
75
+
76
+
77
+ def parse_entity_response(
78
+ response: httpx.Response,
79
+ ) -> tuple[list[tuple[str, str]], str | None]:
80
+ """Parse ``GET /entity`` → ``(endpoints, acumatica_build_version?)``.
81
+
82
+ Endpoints dual-shape per V31. Build id is ``version.acumaticaBuildVersion``
83
+ on the 26.x wrapper only; bare array → ``None`` (T92).
72
84
  """
73
85
  try:
74
86
  body = response.json()
@@ -80,15 +92,17 @@ def parse_entity_list(response: httpx.Response) -> list[tuple[str, str]]:
80
92
  f"content-type {response.headers.get('content-type', '?')}; "
81
93
  f"first 200 chars: {hint})"
82
94
  ) from exc
83
- if not isinstance(body, list) or not body:
95
+ rows = _entity_list_rows(body)
96
+ if rows is None:
84
97
  hint = str(body)[:200]
85
98
  raise RuntimeError(
86
99
  "GET /entity response not parseable as endpoint list "
87
- f"(status {response.status_code}; expected non-empty JSON array; "
100
+ f"(status {response.status_code}; expected non-empty JSON array "
101
+ "or object with non-empty 'endpoints' array; "
88
102
  f"first 200 chars: {hint})"
89
103
  )
90
104
  out: list[tuple[str, str]] = []
91
- for item in body:
105
+ for item in rows:
92
106
  if not isinstance(item, dict):
93
107
  raise RuntimeError(
94
108
  "GET /entity response not parseable as endpoint list "
@@ -102,7 +116,31 @@ def parse_entity_list(response: httpx.Response) -> list[tuple[str, str]]:
102
116
  f"(row missing string name/version: {item!r})"
103
117
  )
104
118
  out.append((name, version))
105
- return out
119
+ return out, _entity_build_version(body)
120
+
121
+
122
+ def _entity_list_rows(body: Any) -> list[Any] | None:
123
+ """Return endpoint row list from array or 26.x wrapper; else None."""
124
+ if isinstance(body, list) and body:
125
+ return body
126
+ if isinstance(body, dict):
127
+ endpoints = body.get("endpoints")
128
+ if isinstance(endpoints, list) and endpoints:
129
+ return endpoints
130
+ return None
131
+
132
+
133
+ def _entity_build_version(body: Any) -> str | None:
134
+ """``version.acumaticaBuildVersion`` from 26.x wrapper; else None."""
135
+ if not isinstance(body, dict):
136
+ return None
137
+ version = body.get("version")
138
+ if not isinstance(version, dict):
139
+ return None
140
+ build = version.get("acumaticaBuildVersion")
141
+ if isinstance(build, str) and build:
142
+ return build
143
+ return None
106
144
 
107
145
 
108
146
  # The list GET's optimized-export failure (B9): the contract API's list GET
@@ -245,8 +283,16 @@ class AcumaticaClient:
245
283
 
246
284
  Fail-closed on unparseable body — see ``parse_entity_list``.
247
285
  """
286
+ return self.entity_root()[0]
287
+
288
+ def entity_root(self) -> tuple[list[tuple[str, str]], str | None]:
289
+ """Authenticated ``GET /entity`` → endpoints + optional ERP build id.
290
+
291
+ Build id is present only on the 26.x wrapper
292
+ (``version.acumaticaBuildVersion``). Bare array → ``None`` (T92).
293
+ """
248
294
  r = self._checked(self._http.get("/entity"))
249
- return parse_entity_list(r)
295
+ return parse_entity_response(r)
250
296
 
251
297
  @staticmethod
252
298
  def _checked(r: httpx.Response) -> httpx.Response:
File without changes