icedcoffee-sync 0.1.0.dev5__tar.gz → 0.1.0.dev9__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. icedcoffee_sync-0.1.0.dev9/PKG-INFO +178 -0
  2. icedcoffee_sync-0.1.0.dev9/README.md +167 -0
  3. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/pyproject.toml +2 -1
  4. icedcoffee_sync-0.1.0.dev5/PKG-INFO +0 -9
  5. icedcoffee_sync-0.1.0.dev5/README.md +0 -17
  6. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/.github/workflows/ci.yml +0 -0
  7. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/.gitignore +0 -0
  8. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/.python-version +0 -0
  9. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/LICENSE +0 -0
  10. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/justfile +0 -0
  11. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/__init__.py +0 -0
  12. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/apply/__init__.py +0 -0
  13. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/apply/diff.py +0 -0
  14. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/apply/execute.py +0 -0
  15. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/apply/model.py +0 -0
  16. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/apply/order.py +0 -0
  17. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/apply/park.py +0 -0
  18. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/apply/prune.py +0 -0
  19. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/erpnext/__init__.py +0 -0
  20. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/erpnext/accounts.py +0 -0
  21. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/erpnext/comment.py +0 -0
  22. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/erpnext/company.py +0 -0
  23. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/erpnext/documents.py +0 -0
  24. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/erpnext/ensure.py +0 -0
  25. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/erpnext/purchase.py +0 -0
  26. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/erpnext/webhook.py +0 -0
  27. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/frappe/__init__.py +0 -0
  28. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/frappe/access.py +0 -0
  29. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/frappe/client.py +0 -0
  30. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/frappe/errors.py +0 -0
  31. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/frappe/lifecycle.py +0 -0
  32. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/frappe/request.py +0 -0
  33. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/frappe/response.py +0 -0
  34. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/py.typed +0 -0
  35. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/src/icedcoffee_sync/site.py +0 -0
  36. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/__init__.py +0 -0
  37. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/capture_fixtures.py +0 -0
  38. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/fixtures/README.md +0 -0
  39. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/fixtures/duplicate_not_provoked.json +0 -0
  40. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/fixtures/error_not_found.json +0 -0
  41. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/fixtures/error_validation.json +0 -0
  42. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/fixtures/get_ok.json +0 -0
  43. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/fixtures/list_ok.json +0 -0
  44. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/fixtures/permission_not_provoked.json +0 -0
  45. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/fixtures/purchase_order_submitted.json +0 -0
  46. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_apply_diff.py +0 -0
  47. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_apply_execute.py +0 -0
  48. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_apply_order.py +0 -0
  49. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_apply_park.py +0 -0
  50. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_apply_prune.py +0 -0
  51. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_client.py +0 -0
  52. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_download_file.py +0 -0
  53. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_erpnext_accounts.py +0 -0
  54. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_erpnext_accounts_reads.py +0 -0
  55. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_erpnext_comment.py +0 -0
  56. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_erpnext_company.py +0 -0
  57. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_erpnext_documents.py +0 -0
  58. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_erpnext_ensure.py +0 -0
  59. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_erpnext_purchase.py +0 -0
  60. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_erpnext_webhook.py +0 -0
  61. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_errors.py +0 -0
  62. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_frappe_access.py +0 -0
  63. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_lifecycle.py +0 -0
  64. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_live_frappe.py +0 -0
  65. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_package.py +0 -0
  66. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_pagination.py +0 -0
  67. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_request.py +0 -0
  68. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_response.py +0 -0
  69. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/tests/test_site.py +0 -0
  70. {icedcoffee_sync-0.1.0.dev5 → icedcoffee_sync-0.1.0.dev9}/uv.lock +0 -0
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.5
2
+ Name: icedcoffee-sync
3
+ Version: 0.1.0.dev9
4
+ Summary: Typed sync and async client for the Frappe and ERPNext REST API
5
+ Author-email: Gavin Carothers <gavin@carothers.name>
6
+ License-Expression: Apache-2.0
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.14
9
+ Requires-Dist: httpx2>=2.0.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # icedcoffee-sync
13
+
14
+ icedcoffee-sync has three layers: a typed Frappe/ERPNext REST client,
15
+ idempotent document provisioning, and reconciliation of a declared set against
16
+ a site. This is version 0.1.0; today the diff/apply/prune engine handles charts
17
+ of accounts only. The intention is to extend that process to other document types.
18
+
19
+ [Source on GitHub](https://github.com/transpire-dev/icedcoffee-sync)
20
+
21
+ ## Installation
22
+
23
+ Requires Python 3.14. To include pre-releases:
24
+
25
+ ```sh
26
+ pip install --pre icedcoffee-sync
27
+ # or
28
+ uv add --prerelease allow icedcoffee-sync
29
+ ```
30
+
31
+ ## Client
32
+
33
+ `icedcoffee_sync.frappe.client` provides typed sync and async clients.
34
+ `SiteConfig.may_submit` defaults to false, guarding against accidental submits,
35
+ cancellations, known committing methods, and document writes that set `docstatus`.
36
+ The API key's server-side permissions enforce access.
37
+
38
+ Replace the site URL, API credentials, and document names in these examples.
39
+ Both clients return document dictionaries from reads and close connections on exit.
40
+
41
+ ```python
42
+ from icedcoffee_sync.frappe.client import Client, SiteConfig
43
+
44
+ config = SiteConfig(
45
+ url="https://erp.example.com",
46
+ api_key="your-api-key",
47
+ api_secret="your-api-secret",
48
+ )
49
+ with Client(config) as client:
50
+ company = client.get("Company", "Example Company")
51
+ print(company["name"])
52
+ ```
53
+
54
+ The async equivalent:
55
+
56
+ ```python
57
+ import asyncio
58
+ from icedcoffee_sync.frappe.client import AsyncClient, SiteConfig
59
+
60
+ config = SiteConfig(
61
+ url="https://erp.example.com",
62
+ api_key="your-api-key",
63
+ api_secret="your-api-secret",
64
+ )
65
+
66
+
67
+ async def main():
68
+ async with AsyncClient(config) as client:
69
+ company = await client.get("Company", "Example Company")
70
+ print(company["name"])
71
+
72
+
73
+ asyncio.run(main())
74
+ ```
75
+
76
+ - `icedcoffee_sync.frappe.access`: provision roles, permission grants, users and API keys, and probe that a key cannot submit.
77
+ - `icedcoffee_sync.site.require_safe_site_url`: call before sending credentials. It refuses a URL that carries a credential, has no hostname, is not HTTPS on port 443, has a path, query or fragment past the site root, or names a host the caller refused. The clients do not call it for you.
78
+
79
+ ## Document provisioning
80
+
81
+ `icedcoffee_sync.erpnext.ensure.ensure_by_name` gets a document by name and
82
+ inserts it only if missing, never rewriting one a person changed. It returns
83
+ true for an insert and false for an existing document. Supply a dict whose
84
+ inserted document will have the name you check. This get-or-insert pattern is
85
+ the provisioning layer for Supplier, Item, Purchase Order, Custom Field and
86
+ Webhook documents today.
87
+
88
+ ```python
89
+ from icedcoffee_sync.erpnext.ensure import ensure_by_name
90
+ from icedcoffee_sync.frappe.client import Client, SiteConfig
91
+
92
+ config = SiteConfig(
93
+ url="https://erp.example.com",
94
+ api_key="your-api-key",
95
+ api_secret="your-api-secret",
96
+ )
97
+ with Client(config) as client:
98
+ created = ensure_by_name(
99
+ client,
100
+ "Item",
101
+ "EXAMPLE-ITEM",
102
+ {
103
+ "item_code": "EXAMPLE-ITEM",
104
+ "item_name": "Example Item",
105
+ "item_group": "All Item Groups",
106
+ "stock_uom": "Nos",
107
+ },
108
+ )
109
+ print(created)
110
+ ```
111
+
112
+ - `icedcoffee_sync.erpnext.purchase`: typed Purchase Order reads and draft Purchase Receipt documents.
113
+ - `icedcoffee_sync.erpnext.company`: validate, set and clear Company default-account links.
114
+ - `icedcoffee_sync.erpnext.comment`: add and list comments on documents.
115
+
116
+ ## Reconciliation
117
+
118
+ `icedcoffee_sync.apply` reconciles charts through the synchronous client.
119
+ Declare account numbers and parent numbers; roots must already exist and are
120
+ matched by root type. This example declares an expense root and one child.
121
+ Before pruning, declare **every account you want to keep** in the company.
122
+
123
+ **Warning:** `apply_chart` and `execute_prune` write to the site. Review each
124
+ printed plan before continuing; the example pauses before each write step.
125
+
126
+ ```python
127
+ from icedcoffee_sync.apply.diff import diff
128
+ from icedcoffee_sync.apply.execute import apply_chart
129
+ from icedcoffee_sync.apply.model import DesiredAccount
130
+ from icedcoffee_sync.apply.prune import execute_prune, plan_prune
131
+ from icedcoffee_sync.erpnext.accounts import accounts_with_entries, company_abbr, read_chart
132
+ from icedcoffee_sync.frappe.client import Client, SiteConfig
133
+
134
+ config = SiteConfig(
135
+ url="https://erp.example.com",
136
+ api_key="your-api-key",
137
+ api_secret="your-api-secret",
138
+ )
139
+ company = "Example Company"
140
+ desired = [
141
+ DesiredAccount("5000", "Expenses", None, True, "Expense"),
142
+ DesiredAccount("5202", "Bank Charges", "5000", False, "Expense"),
143
+ ]
144
+ with Client(config) as client:
145
+ live = read_chart(client, company)
146
+ entries = accounts_with_entries(client, [a.document_name for a in live])
147
+ plan = diff(desired, live, entries)
148
+ print(plan)
149
+ input("Review the diff; Enter to apply, Ctrl-C to stop: ")
150
+ print(apply_chart(client, plan, live, {a.number: a for a in desired}, company_abbr(client, company), company))
151
+
152
+ live = read_chart(client, company)
153
+ entries = accounts_with_entries(client, [a.document_name for a in live])
154
+ plan = diff(desired, live, entries)
155
+ pruning = plan_prune([extra.live for extra in plan.extras], live, entries)
156
+ print(pruning)
157
+ input("Review the prune plan; Enter to execute, Ctrl-C to stop: ")
158
+ print(execute_prune(client, pruning))
159
+ ```
160
+
161
+ `diff` reports creates, renames, type changes, re-enables, extras and refusals
162
+ without writes. `apply_chart` runs changes in dependency order; any refusal
163
+ blocks the entire apply, and extras are left alone. `plan_prune` protects roots,
164
+ accounts with ledger entries and parents with surviving children.
165
+ `execute_prune` deletes selected extras, falling back to disabling them for
166
+ certain ERPNext business-rule errors.
167
+
168
+ ## Development
169
+
170
+ Requires Python 3.14 and [uv](https://docs.astral.sh/uv/).
171
+
172
+ just install # create the virtualenv
173
+ just check # lock, lint, typecheck, deptry
174
+ just test # pytest, excluding tests that need a live site
175
+ just lint-fix # apply ruff fixes
176
+
177
+ Tests marked `live_frappe` hit a real Frappe site and need the
178
+ `ICEDCOFFEE_TEST_*` environment variables; `just test` skips them.
@@ -0,0 +1,167 @@
1
+ # icedcoffee-sync
2
+
3
+ icedcoffee-sync has three layers: a typed Frappe/ERPNext REST client,
4
+ idempotent document provisioning, and reconciliation of a declared set against
5
+ a site. This is version 0.1.0; today the diff/apply/prune engine handles charts
6
+ of accounts only. The intention is to extend that process to other document types.
7
+
8
+ [Source on GitHub](https://github.com/transpire-dev/icedcoffee-sync)
9
+
10
+ ## Installation
11
+
12
+ Requires Python 3.14. To include pre-releases:
13
+
14
+ ```sh
15
+ pip install --pre icedcoffee-sync
16
+ # or
17
+ uv add --prerelease allow icedcoffee-sync
18
+ ```
19
+
20
+ ## Client
21
+
22
+ `icedcoffee_sync.frappe.client` provides typed sync and async clients.
23
+ `SiteConfig.may_submit` defaults to false, guarding against accidental submits,
24
+ cancellations, known committing methods, and document writes that set `docstatus`.
25
+ The API key's server-side permissions enforce access.
26
+
27
+ Replace the site URL, API credentials, and document names in these examples.
28
+ Both clients return document dictionaries from reads and close connections on exit.
29
+
30
+ ```python
31
+ from icedcoffee_sync.frappe.client import Client, SiteConfig
32
+
33
+ config = SiteConfig(
34
+ url="https://erp.example.com",
35
+ api_key="your-api-key",
36
+ api_secret="your-api-secret",
37
+ )
38
+ with Client(config) as client:
39
+ company = client.get("Company", "Example Company")
40
+ print(company["name"])
41
+ ```
42
+
43
+ The async equivalent:
44
+
45
+ ```python
46
+ import asyncio
47
+ from icedcoffee_sync.frappe.client import AsyncClient, SiteConfig
48
+
49
+ config = SiteConfig(
50
+ url="https://erp.example.com",
51
+ api_key="your-api-key",
52
+ api_secret="your-api-secret",
53
+ )
54
+
55
+
56
+ async def main():
57
+ async with AsyncClient(config) as client:
58
+ company = await client.get("Company", "Example Company")
59
+ print(company["name"])
60
+
61
+
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ - `icedcoffee_sync.frappe.access`: provision roles, permission grants, users and API keys, and probe that a key cannot submit.
66
+ - `icedcoffee_sync.site.require_safe_site_url`: call before sending credentials. It refuses a URL that carries a credential, has no hostname, is not HTTPS on port 443, has a path, query or fragment past the site root, or names a host the caller refused. The clients do not call it for you.
67
+
68
+ ## Document provisioning
69
+
70
+ `icedcoffee_sync.erpnext.ensure.ensure_by_name` gets a document by name and
71
+ inserts it only if missing, never rewriting one a person changed. It returns
72
+ true for an insert and false for an existing document. Supply a dict whose
73
+ inserted document will have the name you check. This get-or-insert pattern is
74
+ the provisioning layer for Supplier, Item, Purchase Order, Custom Field and
75
+ Webhook documents today.
76
+
77
+ ```python
78
+ from icedcoffee_sync.erpnext.ensure import ensure_by_name
79
+ from icedcoffee_sync.frappe.client import Client, SiteConfig
80
+
81
+ config = SiteConfig(
82
+ url="https://erp.example.com",
83
+ api_key="your-api-key",
84
+ api_secret="your-api-secret",
85
+ )
86
+ with Client(config) as client:
87
+ created = ensure_by_name(
88
+ client,
89
+ "Item",
90
+ "EXAMPLE-ITEM",
91
+ {
92
+ "item_code": "EXAMPLE-ITEM",
93
+ "item_name": "Example Item",
94
+ "item_group": "All Item Groups",
95
+ "stock_uom": "Nos",
96
+ },
97
+ )
98
+ print(created)
99
+ ```
100
+
101
+ - `icedcoffee_sync.erpnext.purchase`: typed Purchase Order reads and draft Purchase Receipt documents.
102
+ - `icedcoffee_sync.erpnext.company`: validate, set and clear Company default-account links.
103
+ - `icedcoffee_sync.erpnext.comment`: add and list comments on documents.
104
+
105
+ ## Reconciliation
106
+
107
+ `icedcoffee_sync.apply` reconciles charts through the synchronous client.
108
+ Declare account numbers and parent numbers; roots must already exist and are
109
+ matched by root type. This example declares an expense root and one child.
110
+ Before pruning, declare **every account you want to keep** in the company.
111
+
112
+ **Warning:** `apply_chart` and `execute_prune` write to the site. Review each
113
+ printed plan before continuing; the example pauses before each write step.
114
+
115
+ ```python
116
+ from icedcoffee_sync.apply.diff import diff
117
+ from icedcoffee_sync.apply.execute import apply_chart
118
+ from icedcoffee_sync.apply.model import DesiredAccount
119
+ from icedcoffee_sync.apply.prune import execute_prune, plan_prune
120
+ from icedcoffee_sync.erpnext.accounts import accounts_with_entries, company_abbr, read_chart
121
+ from icedcoffee_sync.frappe.client import Client, SiteConfig
122
+
123
+ config = SiteConfig(
124
+ url="https://erp.example.com",
125
+ api_key="your-api-key",
126
+ api_secret="your-api-secret",
127
+ )
128
+ company = "Example Company"
129
+ desired = [
130
+ DesiredAccount("5000", "Expenses", None, True, "Expense"),
131
+ DesiredAccount("5202", "Bank Charges", "5000", False, "Expense"),
132
+ ]
133
+ with Client(config) as client:
134
+ live = read_chart(client, company)
135
+ entries = accounts_with_entries(client, [a.document_name for a in live])
136
+ plan = diff(desired, live, entries)
137
+ print(plan)
138
+ input("Review the diff; Enter to apply, Ctrl-C to stop: ")
139
+ print(apply_chart(client, plan, live, {a.number: a for a in desired}, company_abbr(client, company), company))
140
+
141
+ live = read_chart(client, company)
142
+ entries = accounts_with_entries(client, [a.document_name for a in live])
143
+ plan = diff(desired, live, entries)
144
+ pruning = plan_prune([extra.live for extra in plan.extras], live, entries)
145
+ print(pruning)
146
+ input("Review the prune plan; Enter to execute, Ctrl-C to stop: ")
147
+ print(execute_prune(client, pruning))
148
+ ```
149
+
150
+ `diff` reports creates, renames, type changes, re-enables, extras and refusals
151
+ without writes. `apply_chart` runs changes in dependency order; any refusal
152
+ blocks the entire apply, and extras are left alone. `plan_prune` protects roots,
153
+ accounts with ledger entries and parents with surviving children.
154
+ `execute_prune` deletes selected extras, falling back to disabling them for
155
+ certain ERPNext business-rule errors.
156
+
157
+ ## Development
158
+
159
+ Requires Python 3.14 and [uv](https://docs.astral.sh/uv/).
160
+
161
+ just install # create the virtualenv
162
+ just check # lock, lint, typecheck, deptry
163
+ just test # pytest, excluding tests that need a live site
164
+ just lint-fix # apply ruff fixes
165
+
166
+ Tests marked `live_frappe` hit a real Frappe site and need the
167
+ `ICEDCOFFEE_TEST_*` environment variables; `just test` skips them.
@@ -1,7 +1,8 @@
1
1
  [project]
2
2
  name = "icedcoffee-sync"
3
- version = "0.1.0.dev5"
3
+ version = "0.1.0.dev9"
4
4
  description = "Typed sync and async client for the Frappe and ERPNext REST API"
5
+ readme = "README.md"
5
6
  authors = [{ name = "Gavin Carothers", email = "gavin@carothers.name" }]
6
7
  license = "Apache-2.0"
7
8
  license-files = ["LICENSE"]
@@ -1,9 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: icedcoffee-sync
3
- Version: 0.1.0.dev5
4
- Summary: Typed sync and async client for the Frappe and ERPNext REST API
5
- Author-email: Gavin Carothers <gavin@carothers.name>
6
- License-Expression: Apache-2.0
7
- License-File: LICENSE
8
- Requires-Python: >=3.14
9
- Requires-Dist: httpx2>=2.0.0
@@ -1,17 +0,0 @@
1
- icedcoffee-sync is a typed client and sync engine for Frappe/ERPNext sites.
2
-
3
- It provides a sync and async client for the Frappe REST API and a
4
- diff/apply/prune engine that brings a site's documents in line with a declared
5
- set.
6
-
7
- ## Development
8
-
9
- Requires Python 3.14 and [uv](https://docs.astral.sh/uv/).
10
-
11
- just install # create the virtualenv
12
- just check # lock, lint, typecheck, deptry
13
- just test # pytest, excluding tests that need a live site
14
- just lint-fix # apply ruff fixes
15
-
16
- Tests marked `live_frappe` hit a real Frappe site and need the
17
- `ICEDCOFFEE_TEST_*` environment variables; `just test` skips them.