cascade-cms-rest 3.2.2__tar.gz → 3.4.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.
Files changed (38) hide show
  1. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/.github/workflows/release.yml +1 -15
  2. cascade_cms_rest-3.4.0/AGENTS.md +63 -0
  3. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/CHANGELOG.md +44 -0
  4. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/PKG-INFO +69 -18
  5. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/PUBLISHING.md +10 -17
  6. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/README.md +67 -15
  7. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/pyproject.toml +2 -3
  8. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/__init__.py +1 -1
  9. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/cmstypes.py +3 -9
  10. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/driver.py +19 -101
  11. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/operations.py +131 -4
  12. cascade_cms_rest-3.4.0/src/cascade_cms/utils/__init__.py +5 -0
  13. {cascade_cms_rest-3.2.2/src/cascade_cms → cascade_cms_rest-3.4.0/src/cascade_cms/utils}/operation_logger.py +54 -20
  14. cascade_cms_rest-3.4.0/src/cascade_cms/utils/redaction.py +16 -0
  15. cascade_cms_rest-3.4.0/src/cascade_cms/utils/script_notes.py +77 -0
  16. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/wrapper.py +68 -22
  17. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/edit_test.py +0 -1
  18. cascade_cms_rest-3.4.0/tests/test_driver.py +35 -0
  19. cascade_cms_rest-3.4.0/tests/test_logging_exit.py +228 -0
  20. cascade_cms_rest-3.4.0/tests/test_non200.py +118 -0
  21. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/test_operation_chains.py +1 -1
  22. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/test_operation_logger.py +1 -1
  23. cascade_cms_rest-3.4.0/tests/test_redaction.py +30 -0
  24. cascade_cms_rest-3.4.0/tests/test_result_lines.py +126 -0
  25. cascade_cms_rest-3.4.0/tests/test_script_log.py +153 -0
  26. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/test_wrapper.py +1 -1
  27. cascade_cms_rest-3.2.2/AGENTS.md +0 -43
  28. cascade_cms_rest-3.2.2/tests/test_driver.py +0 -14
  29. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/.github/copilot-instructions.md +0 -0
  30. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/.github/workflows/ci.yml +0 -0
  31. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/.github/workflows/dependency-review.yml +0 -0
  32. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/.gitignore +0 -0
  33. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/LICENSE +0 -0
  34. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/failures.py +0 -0
  35. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/py.typed +0 -0
  36. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/conftest.py +0 -0
  37. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/test_cmstypes.py +0 -0
  38. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/test_operations.py +0 -0
@@ -6,7 +6,7 @@ on:
6
6
  - "v*.*.*"
7
7
 
8
8
  permissions:
9
- contents: write
9
+ contents: read
10
10
  id-token: write
11
11
 
12
12
  jobs:
@@ -28,20 +28,6 @@ jobs:
28
28
  name: dist
29
29
  path: dist/
30
30
 
31
- release:
32
- needs: build
33
- runs-on: ubuntu-latest
34
- steps:
35
- - uses: actions/download-artifact@v4
36
- with:
37
- name: dist
38
- path: dist/
39
- - name: Create GitHub release
40
- uses: softprops/action-gh-release@v2
41
- with:
42
- files: dist/*
43
- generate_release_notes: true
44
-
45
31
  publish:
46
32
  name: Publish to PyPI
47
33
  needs: build
@@ -0,0 +1,63 @@
1
+ # AGENTS.md
2
+
3
+ `cascade-cms-rest` — a typed, async REST client for Hannon Hill Cascade CMS 8.
4
+ Library source is `src/cascade_cms/`.
5
+
6
+
7
+ ## Repo commands
8
+
9
+ ```bash
10
+ pip install -e ".[dev]"
11
+ pytest
12
+ ruff check .
13
+ mypy src/
14
+ ```
15
+
16
+ CI runs `ruff check .`, `mypy src/`, and `pytest`. Run all three before
17
+ considering a change to the library done.
18
+
19
+ ## Writing scripts that use this library
20
+
21
+ Script-writing skills (`cascade-script-writer`/`cascade-script-writer-lite`)
22
+ and the MCP server both moved to their own repo, `cascade-cms-tools`
23
+ (sibling to this one), so this library repo stays dependency-light and
24
+ focused on `src/cascade_cms/`. See that repo's `AGENTS.md` for the
25
+ skill-authoring workflow and how to rebuild release artifacts — both depend
26
+ on the *published* `cascade-cms-rest` package, not a local copy, so nothing
27
+ here needs to stay in sync with them beyond tagging a release.
28
+
29
+ ## Logging and console output
30
+
31
+ - `CascadeWrapperBase(environmentVariables, debug=None, *, exit_on_failure=True,
32
+ log_dir=None)` — there is no response cache and no `configurationVariables`.
33
+ - Status lines (`[INIT]`, `[LOG]: <path>`, `[DONE]`, the tally, `[EXIT]`, console `[ERROR]`)
34
+ go to **stderr**; stdout belongs to the caller's script.
35
+ - The logfile records the tally line and, last, `[EXIT-CODE]: <outcome>` (see the README's
36
+ Logging section for the outcomes). Agents verify a run by reading the logfile at the
37
+ `[LOG]` path, not by capturing console output.
38
+ - `log_dir` (explicit parameter) beats a debug config's `log_dir`; default `./logs`. Log
39
+ filenames are `{SERVER}[_debug]_{timestamp}_{n}.log`.
40
+ - Successful writes log `[RESULT]: ...` automatically; scripts log `[NOTE]: <text>` with
41
+ `from cascade_cms.utils import script_log` then `script_log.note(text)` inside the `with`
42
+ block (formats in the README). Process-pool callbacks return values and the main script notes
43
+ them.
44
+ - Package layout: `cascade_cms/utils/` holds `operation_logger.py`, `redaction.py` and
45
+ `script_notes.py`. `cascade_cms.operation_logger` and `cascade_cms.redaction` no longer exist
46
+ (no shim); `from cascade_cms import OperationLogger` still works.
47
+ - The API bearer token is masked (`cascade_cms.utils.redaction.mask_token`) wherever it could surface.
48
+
49
+ ## Maintaining the CHANGELOG
50
+
51
+ Each section in `CHANGELOG.md` should include a note at the bottom listing
52
+ which files were touched. For example:
53
+
54
+ ```markdown
55
+ ### Fixed
56
+ - Fixed async callback execution order (affects callback chains)
57
+ - Fixed nested payload aliasing issue
58
+
59
+ **Files changed:** `src/cascade_cms/operations.py`, `src/cascade_cms/cmstypes.py`
60
+ ```
61
+
62
+ This helps readers quickly understand the scope of changes and identify which
63
+ parts of the library were affected.
@@ -2,6 +2,50 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.4.0]
6
+
7
+ ### Added
8
+ - **`[RESULT]` lines:** each successful write operation now writes one line to the logfile, after its chain's pipeline line, naming what Cascade returned (`create` → new type, id and path; other writes → operation, target and `succeeded`). Reads and failed writes write none. See the README for the exact format.
9
+ - **`script_log.note(text)`** (`from cascade_cms.utils import script_log`): writes `[NOTE]: <text>` to the active run's logfile. A stateless singleton routed through a `ContextVar` set by `CascadeWrapperBase`'s `with` block, so concurrent wrappers on different threads keep their notes separate. Called outside a run it drops the note and emits a `RuntimeWarning`. API keys in the text are masked.
10
+ - `cascade_cms/utils/` package.
11
+
12
+ ### Changed
13
+ - **BREAKING: `cascade_cms.operation_logger` moved to `cascade_cms.utils.operation_logger`**, with no shim at the old path. `from cascade_cms import OperationLogger` still works.
14
+ - **BREAKING: `cascade_cms.redaction` (new in 3.3.0) moved to `cascade_cms.utils.redaction`**, also with no shim.
15
+ - Sync callbacks run in a thread pool now execute in a copy of the caller's context (neither `run_in_executor` nor `Executor.submit` copies it), so `script_log.note()` works from them. Process-pool callbacks are unchanged and cannot call `note()`.
16
+
17
+ **Files changed:** `src/cascade_cms/utils/` (added: `__init__.py`, `script_notes.py`; moved: `operation_logger.py`, `redaction.py`), `src/cascade_cms/operations.py`, `src/cascade_cms/wrapper.py`, `src/cascade_cms/driver.py`, `src/cascade_cms/__init__.py`, `pyproject.toml`, `README.md`, `AGENTS.md`, `tests/test_result_lines.py` (added), `tests/test_script_log.py` (added), `tests/test_logging_exit.py`, `tests/test_operation_chains.py`, `tests/test_operation_logger.py`, `tests/test_redaction.py`, `tests/test_wrapper.py`
18
+
19
+ ## [3.3.0]
20
+
21
+ ### Removed
22
+ - **BREAKING: response caching is removed entirely** (no persistent and no in-memory cache): `CacheHandler`, `default_cache_backend()`, `OperationLogger.log_cache_hit()` and `ResponseParser._cacheable`. No `./cache` directory is created.
23
+ - **BREAKING: `configurationVariables` is removed from `CascadeWrapperBase`** with no deprecation period. `CascadeWrapperBase(environmentVariables, debug=None, *, exit_on_failure=True, log_dir=None)`; `exit_on_failure` is now keyword-only.
24
+ - **BREAKING: `backendConfig` is removed from `CascadeCMSRestDriver`.**
25
+ - **BREAKING: `python-dotenv` is no longer a dependency**, and `aiohttp-client-cache` is dropped. `aiohttp` is now a direct dependency (`>=3.14.3`).
26
+
27
+ ### Changed
28
+ - **BREAKING: status lines go to stderr instead of stdout** (`[INIT]`, `[DEBUG]`, `[DONE]`, the tally, `[EXIT]`, console `[ERROR]`). Anything reading the tally from stdout must read stderr or the logfile.
29
+ - **BREAKING: a non-200 HTTP status is now a `CascadeError` (category CASCADE, message `"<status> <reason>"`)**, no longer a NETWORK failure. The response body is never read or logged.
30
+ - **BREAKING: `ResponseParser.serializer` is now required**; the `RuntimeWarning` branch is gone.
31
+ - **BREAKING: `RequestExecutor.parser` is now required** (the unused default was removed). `RequestExecutor` is exported from `cascade_cms/__init__.py`.
32
+ - Log filenames now include microseconds and a per-process serial (`{SERVER}[_debug]_{timestamp}_{n}.log`), so two wrappers created in the same second never share a file.
33
+ - A batch-level failure's prefix follows its cause: `[NETWORK]` for network causes, `[CASCADE-REST-CMS]` otherwise.
34
+ - `CascadeCMSRestDriver.close()` clears the closed event loop as the current loop.
35
+ - Release process: the tag-triggered workflow no longer creates a GitHub release (PyPI publishing on tag push is unchanged, with `contents: read`), and `PUBLISHING.md` no longer has a manual `twine upload` step.
36
+
37
+ ### Added
38
+ - `log_dir` keyword-only parameter on `CascadeWrapperBase` (normal and debug mode; created if missing; wins over a debug config's `log_dir`).
39
+ - `[LOG]: <path>` is printed to stderr right after `[INIT]`.
40
+ - The logfile records the tally line and a final `[EXIT-CODE]: <outcome>` line.
41
+ - `cascade_cms.redaction.mask_token()`; the debug logfile masks the `Authorization` header to its last four characters (`Bearer ****abcd`).
42
+
43
+ ### Fixed
44
+ - Each wrapper closes and releases its logfile handle on exit.
45
+ - Verified (Python 3.12): Cascade's `Partitioned` session cookie no longer breaks requests now that the cache library is gone; no compatibility patch is needed in the library.
46
+
47
+ **Files changed:** `src/cascade_cms/driver.py`, `src/cascade_cms/wrapper.py`, `src/cascade_cms/operation_logger.py`, `src/cascade_cms/cmstypes.py`, `src/cascade_cms/redaction.py` (added), `pyproject.toml`, `.github/workflows/release.yml`, `PUBLISHING.md`, `README.md`, `AGENTS.md`, `tests/test_driver.py`, `tests/test_non200.py` (added), `tests/test_redaction.py` (added), `tests/test_logging_exit.py` (added), `tests/edit_test.py`
48
+
5
49
  ## [3.2.2]
6
50
 
7
51
  ### Added
@@ -1,15 +1,14 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cascade-cms-rest
3
- Version: 3.2.2
3
+ Version: 3.4.0
4
4
  Summary: A typed, async REST client for Hannon Hill Cascade CMS
5
5
  Project-URL: Homepage, https://github.com/Sharkdroid/py-cascade-cms
6
6
  Author: Keith Shark
7
7
  License-Expression: MIT
8
8
  License-File: LICENSE
9
9
  Requires-Python: >=3.12
10
- Requires-Dist: aiohttp-client-cache[sqlite]
10
+ Requires-Dist: aiohttp>=3.14.3
11
11
  Requires-Dist: pydantic>=2
12
- Requires-Dist: python-dotenv
13
12
  Provides-Extra: dev
14
13
  Requires-Dist: build; extra == 'dev'
15
14
  Requires-Dist: mypy; extra == 'dev'
@@ -36,13 +35,8 @@ environment_variables = {
36
35
  "CASCADE_URL": "...",
37
36
  "SERVER": "prod", # label used for logfile naming
38
37
  }
39
- configuration_variables = {
40
- "cache_name": "./cache/cache.sqlite",
41
- "allowed_codes": (200,),
42
- "allowed_methods": ("GET",),
43
- }
44
38
 
45
- with CascadeWrapperBase(environment_variables, configuration_variables) as cascade:
39
+ with CascadeWrapperBase(environment_variables) as cascade:
46
40
  identifier = IdentifierType(identifier="e868f539ac1001062cfa029c4c5df4d0", asset_type="folder")
47
41
  cascade.operations.read(identifier)
48
42
  results = cascade.submit_requests(Asset)
@@ -69,7 +63,7 @@ def rewrite(asset):
69
63
  asset.keywords = "updated"
70
64
  return asset
71
65
 
72
- with CascadeWrapperBase(environment_variables, configuration_variables) as cascade:
66
+ with CascadeWrapperBase(environment_variables) as cascade:
73
67
  cascade.operations.read(page_a).edit(page_a, rewrite).publish(page_a)
74
68
  cascade.operations.read(page_b).then(report)
75
69
  cascade.operations.delete(old_page)
@@ -114,14 +108,17 @@ manager owns it end to end:
114
108
  server) — `__exit__` then never raises; read `.success`/`.failed` off the results yourself.
115
109
  - The logfile's `!ERROR:` line is prefixed `[NETWORK]` for a network-layer failure or
116
110
  `[CASCADE-REST-CMS]` for any other library-side failure; a `CascadeError` or a callback
117
- exception gets no prefix.
111
+ exception gets no prefix. A batch-level failure takes the prefix that matches its cause.
112
+ - Cascade always answers HTTP 200 with a JSON body; a non-200 status (an HTML page from the
113
+ server layer, e.g. `501 Not Implemented`) is reported as a `CascadeError` whose message is the
114
+ status code and reason. The HTML body is never read or logged.
118
115
  - At exit, the console always prints a tally: `"{failed} failed, {succeeded} succeeded"`,
119
116
  cumulative across every `submit_requests()` call in the session, with `": reference log for
120
117
  details"` appended only when at least one failure came from the API, the network, or the
121
- library itself (not from a callback-only failure).
118
+ library itself (not from a callback-only failure). The tally is also written to the logfile.
122
119
 
123
120
  ```python
124
- with CascadeWrapperBase(environment_variables, configuration_variables) as cascade:
121
+ with CascadeWrapperBase(environment_variables) as cascade:
125
122
  cascade.operations.read(identifier)
126
123
  results = cascade.submit_requests(Asset)
127
124
  for asset in results.success:
@@ -132,11 +129,24 @@ with CascadeWrapperBase(environment_variables, configuration_variables) as casca
132
129
 
133
130
  ### Logging
134
131
 
135
- `CascadeWrapperBase` accepts an optional third `debug` argument. Leaving it as `None`
136
- (the default) runs in **normal mode**: a minimal console (`[INIT]`/`[RUNNING]`/`Processed: n/N`/
137
- `[DONE]`/`[EXIT]`) plus a simple logfile at `./logs/{SERVER}_{timestamp}.log`. Passing a dict
132
+ `CascadeWrapperBase` accepts an optional second `debug` argument. Leaving it as `None`
133
+ (the default) runs in **normal mode**: a minimal console (`[INIT]`/`[LOG]`/`[RUNNING]`/`n/N succeeded`/
134
+ `[DONE]`/`[EXIT]`) plus a simple logfile at `./logs/{SERVER}_{timestamp}_{n}.log`. Passing a dict
138
135
  switches to **debug mode**: a quiet console and a verbose, nested logfile at
139
- `./logs/{SERVER}_debug_{timestamp}.log` describing every request, response, callback, and error.
136
+ `./logs/{SERVER}_debug_{timestamp}_{n}.log` describing every request, response, callback, and error.
137
+ Each wrapper gets its own file, even when two are created in the same second.
138
+
139
+ - **Status lines go to stderr**, not stdout (`[INIT]`, `[LOG]`, `[DONE]`, the tally, `[EXIT]`,
140
+ console `[ERROR]` lines), so stdout stays free for your script's own output.
141
+ - **`[LOG]: <path>`** is printed right after `[INIT]`, giving the path of this run's logfile.
142
+ - **`log_dir=`** (keyword-only) sets the log directory in normal and debug mode; it is created if
143
+ missing and defaults to `./logs`. If a debug config also sets `log_dir`, the parameter wins.
144
+ - **`[EXIT-CODE]:`** is the last line written to the logfile: `0` for a clean run, `1` when the
145
+ script ends with a failure, `1 (callback exception: <Type>)` / `1 (uncaught exception: <Type>)`
146
+ when an exception ends it, or `n/a (exit_on_failure disabled)`. Read the logfile for the tally
147
+ and exit code rather than capturing console output.
148
+ - In debug mode with `show_network_headers`, the `Authorization` header is masked to its last four
149
+ characters (`Bearer ****abcd`).
140
150
 
141
151
  ```python
142
152
  debug_config = {
@@ -150,10 +160,51 @@ debug_config = {
150
160
  "response_line_limit": 8, # -1 = dump full response body
151
161
  }
152
162
 
153
- with CascadeWrapperBase(environment_variables, configuration_variables, debug=debug_config) as cascade:
163
+ with CascadeWrapperBase(environment_variables, debug=debug_config) as cascade:
154
164
  ...
155
165
  ```
156
166
 
167
+ #### `[RESULT]` and `[NOTE]` lines
168
+
169
+ Two fixed line types make the logfile carry what a script *did*, so an agent (or you) can read it
170
+ instead of capturing stdout:
171
+
172
+ - **`[RESULT]: ...`** is written **automatically**, once per successful write operation
173
+ (`create`, `edit`, `delete`, `copy`, `move`, `publish`, `checkIn`, `checkOut`, `siteCopy`,
174
+ `editAccessRights`, `editWorkflowSettings`, `performWorkflowTransition`, `markMessage`,
175
+ `deleteMessage`, `editPreference`), right after that chain's pipeline line. Reads, searches and
176
+ lists write none; failed writes and failed items of a list create/edit write none. A write that
177
+ succeeded stays on record even if a later step of its chain failed.
178
+
179
+ ```
180
+ [RESULT]: create page 5f1a0000000000000000000000c90d00 /_dev/testing/testbed2-copy
181
+ [RESULT]: delete page 1a2b0000000000000000000000009f00 succeeded
182
+ [RESULT]: siteCopy succeeded
183
+ ```
184
+
185
+ `create` lines are `create <type> <id> [<path>]` (the new asset, as Cascade returned it; `<path>`
186
+ is the site-relative path when the payload gave `parent_folder_path`). Every other write is
187
+ `<operation> [<type> <id> <path>] succeeded`, naming the target (as many of the three as are
188
+ known); an operation with no identifier target is just `<operation> succeeded`. `<id>` is 32
189
+ hex characters and `<path>` starts with `/`.
190
+
191
+ - **`[NOTE]: <text>`** is written **on purpose** by your script:
192
+
193
+ ```python
194
+ from cascade_cms.utils import script_log
195
+
196
+ with CascadeWrapperBase(environment_variables) as cascade:
197
+ script_log.note("will delete page 1a2b... /news/old-1")
198
+ ```
199
+
200
+ `script_log` is a stateless singleton that routes each note to the run whose `with` block is
201
+ active in the calling context, so wrappers open on different threads never mix notes. Notes must
202
+ be written **inside** the `with` block; outside it the note is dropped and a `RuntimeWarning` is
203
+ emitted. Newlines become spaces (one note = one line) and any occurrence of the API key is
204
+ masked. Thread-pool and async callbacks may call `note()` directly. **Process-pool callbacks
205
+ cannot** (another process, no active run): have them return values and note them in the main
206
+ script.
207
+
157
208
  All keys are required in debug mode — there are no inferred defaults, so you always know what
158
209
  you opted into.
159
210
 
@@ -74,21 +74,16 @@
74
74
  git push origin master --tags # Push commits + all tags
75
75
  ```
76
76
 
77
- ### 6. Build & Publish to PyPI
77
+ ### 6. Publishing Happens Automatically
78
78
 
79
- **Build:**
80
- ```bash
81
- python -m build
82
- ```
83
-
84
- This creates `dist/cascade_cms_rest-3.0.2-py3-none-any.whl` and `.tar.gz`
79
+ Pushing the `v*` tag (step 5) triggers the **Release** workflow
80
+ (`.github/workflows/release.yml`), which builds the sdist and wheel and
81
+ publishes them to PyPI. Do **not** run `twine upload` by hand: the tag
82
+ already publishes via CI, so a manual upload collides ("file already
83
+ exists"), and a stale local `dist/` would re-upload old versions.
85
84
 
86
- **Publish:**
87
- ```bash
88
- twine upload dist/*
89
- ```
90
-
91
- You'll be prompted for PyPI credentials (or use `.pypirc` config)
85
+ Watch the workflow run in the GitHub Actions tab, then verify on PyPI
86
+ (below). The workflow does not create a GitHub release.
92
87
 
93
88
  ## Verification
94
89
 
@@ -112,12 +107,10 @@ git commit -m "Bump version to 3.0.2"
112
107
  # 4. Create an annotated tag (v-prefix is convention)
113
108
  git tag -a v3.0.2 -m "Release version 3.0.2"
114
109
 
115
- # 5. Push commits and tags to GitHub
110
+ # 5. Push commits and tags to GitHub; the tag push publishes to PyPI via CI
116
111
  git push origin master --tags
117
112
 
118
- # 6. Build and publish to PyPI
119
- python -m build
120
- twine upload dist/*
113
+ # 6. Verify on PyPI (https://pypi.org/project/cascade-cms-rest/)
121
114
  ```
122
115
 
123
116
  ## Troubleshooting
@@ -15,13 +15,8 @@ environment_variables = {
15
15
  "CASCADE_URL": "...",
16
16
  "SERVER": "prod", # label used for logfile naming
17
17
  }
18
- configuration_variables = {
19
- "cache_name": "./cache/cache.sqlite",
20
- "allowed_codes": (200,),
21
- "allowed_methods": ("GET",),
22
- }
23
18
 
24
- with CascadeWrapperBase(environment_variables, configuration_variables) as cascade:
19
+ with CascadeWrapperBase(environment_variables) as cascade:
25
20
  identifier = IdentifierType(identifier="e868f539ac1001062cfa029c4c5df4d0", asset_type="folder")
26
21
  cascade.operations.read(identifier)
27
22
  results = cascade.submit_requests(Asset)
@@ -48,7 +43,7 @@ def rewrite(asset):
48
43
  asset.keywords = "updated"
49
44
  return asset
50
45
 
51
- with CascadeWrapperBase(environment_variables, configuration_variables) as cascade:
46
+ with CascadeWrapperBase(environment_variables) as cascade:
52
47
  cascade.operations.read(page_a).edit(page_a, rewrite).publish(page_a)
53
48
  cascade.operations.read(page_b).then(report)
54
49
  cascade.operations.delete(old_page)
@@ -93,14 +88,17 @@ manager owns it end to end:
93
88
  server) — `__exit__` then never raises; read `.success`/`.failed` off the results yourself.
94
89
  - The logfile's `!ERROR:` line is prefixed `[NETWORK]` for a network-layer failure or
95
90
  `[CASCADE-REST-CMS]` for any other library-side failure; a `CascadeError` or a callback
96
- exception gets no prefix.
91
+ exception gets no prefix. A batch-level failure takes the prefix that matches its cause.
92
+ - Cascade always answers HTTP 200 with a JSON body; a non-200 status (an HTML page from the
93
+ server layer, e.g. `501 Not Implemented`) is reported as a `CascadeError` whose message is the
94
+ status code and reason. The HTML body is never read or logged.
97
95
  - At exit, the console always prints a tally: `"{failed} failed, {succeeded} succeeded"`,
98
96
  cumulative across every `submit_requests()` call in the session, with `": reference log for
99
97
  details"` appended only when at least one failure came from the API, the network, or the
100
- library itself (not from a callback-only failure).
98
+ library itself (not from a callback-only failure). The tally is also written to the logfile.
101
99
 
102
100
  ```python
103
- with CascadeWrapperBase(environment_variables, configuration_variables) as cascade:
101
+ with CascadeWrapperBase(environment_variables) as cascade:
104
102
  cascade.operations.read(identifier)
105
103
  results = cascade.submit_requests(Asset)
106
104
  for asset in results.success:
@@ -111,11 +109,24 @@ with CascadeWrapperBase(environment_variables, configuration_variables) as casca
111
109
 
112
110
  ### Logging
113
111
 
114
- `CascadeWrapperBase` accepts an optional third `debug` argument. Leaving it as `None`
115
- (the default) runs in **normal mode**: a minimal console (`[INIT]`/`[RUNNING]`/`Processed: n/N`/
116
- `[DONE]`/`[EXIT]`) plus a simple logfile at `./logs/{SERVER}_{timestamp}.log`. Passing a dict
112
+ `CascadeWrapperBase` accepts an optional second `debug` argument. Leaving it as `None`
113
+ (the default) runs in **normal mode**: a minimal console (`[INIT]`/`[LOG]`/`[RUNNING]`/`n/N succeeded`/
114
+ `[DONE]`/`[EXIT]`) plus a simple logfile at `./logs/{SERVER}_{timestamp}_{n}.log`. Passing a dict
117
115
  switches to **debug mode**: a quiet console and a verbose, nested logfile at
118
- `./logs/{SERVER}_debug_{timestamp}.log` describing every request, response, callback, and error.
116
+ `./logs/{SERVER}_debug_{timestamp}_{n}.log` describing every request, response, callback, and error.
117
+ Each wrapper gets its own file, even when two are created in the same second.
118
+
119
+ - **Status lines go to stderr**, not stdout (`[INIT]`, `[LOG]`, `[DONE]`, the tally, `[EXIT]`,
120
+ console `[ERROR]` lines), so stdout stays free for your script's own output.
121
+ - **`[LOG]: <path>`** is printed right after `[INIT]`, giving the path of this run's logfile.
122
+ - **`log_dir=`** (keyword-only) sets the log directory in normal and debug mode; it is created if
123
+ missing and defaults to `./logs`. If a debug config also sets `log_dir`, the parameter wins.
124
+ - **`[EXIT-CODE]:`** is the last line written to the logfile: `0` for a clean run, `1` when the
125
+ script ends with a failure, `1 (callback exception: <Type>)` / `1 (uncaught exception: <Type>)`
126
+ when an exception ends it, or `n/a (exit_on_failure disabled)`. Read the logfile for the tally
127
+ and exit code rather than capturing console output.
128
+ - In debug mode with `show_network_headers`, the `Authorization` header is masked to its last four
129
+ characters (`Bearer ****abcd`).
119
130
 
120
131
  ```python
121
132
  debug_config = {
@@ -129,10 +140,51 @@ debug_config = {
129
140
  "response_line_limit": 8, # -1 = dump full response body
130
141
  }
131
142
 
132
- with CascadeWrapperBase(environment_variables, configuration_variables, debug=debug_config) as cascade:
143
+ with CascadeWrapperBase(environment_variables, debug=debug_config) as cascade:
133
144
  ...
134
145
  ```
135
146
 
147
+ #### `[RESULT]` and `[NOTE]` lines
148
+
149
+ Two fixed line types make the logfile carry what a script *did*, so an agent (or you) can read it
150
+ instead of capturing stdout:
151
+
152
+ - **`[RESULT]: ...`** is written **automatically**, once per successful write operation
153
+ (`create`, `edit`, `delete`, `copy`, `move`, `publish`, `checkIn`, `checkOut`, `siteCopy`,
154
+ `editAccessRights`, `editWorkflowSettings`, `performWorkflowTransition`, `markMessage`,
155
+ `deleteMessage`, `editPreference`), right after that chain's pipeline line. Reads, searches and
156
+ lists write none; failed writes and failed items of a list create/edit write none. A write that
157
+ succeeded stays on record even if a later step of its chain failed.
158
+
159
+ ```
160
+ [RESULT]: create page 5f1a0000000000000000000000c90d00 /_dev/testing/testbed2-copy
161
+ [RESULT]: delete page 1a2b0000000000000000000000009f00 succeeded
162
+ [RESULT]: siteCopy succeeded
163
+ ```
164
+
165
+ `create` lines are `create <type> <id> [<path>]` (the new asset, as Cascade returned it; `<path>`
166
+ is the site-relative path when the payload gave `parent_folder_path`). Every other write is
167
+ `<operation> [<type> <id> <path>] succeeded`, naming the target (as many of the three as are
168
+ known); an operation with no identifier target is just `<operation> succeeded`. `<id>` is 32
169
+ hex characters and `<path>` starts with `/`.
170
+
171
+ - **`[NOTE]: <text>`** is written **on purpose** by your script:
172
+
173
+ ```python
174
+ from cascade_cms.utils import script_log
175
+
176
+ with CascadeWrapperBase(environment_variables) as cascade:
177
+ script_log.note("will delete page 1a2b... /news/old-1")
178
+ ```
179
+
180
+ `script_log` is a stateless singleton that routes each note to the run whose `with` block is
181
+ active in the calling context, so wrappers open on different threads never mix notes. Notes must
182
+ be written **inside** the `with` block; outside it the note is dropped and a `RuntimeWarning` is
183
+ emitted. Newlines become spaces (one note = one line) and any occurrence of the API key is
184
+ masked. Thread-pool and async callbacks may call `note()` directly. **Process-pool callbacks
185
+ cannot** (another process, no active run): have them return values and note them in the main
186
+ script.
187
+
136
188
  All keys are required in debug mode — there are no inferred defaults, so you always know what
137
189
  you opted into.
138
190
 
@@ -4,15 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cascade-cms-rest"
7
- version = "3.2.2"
7
+ version = "3.4.0"
8
8
  description = "A typed, async REST client for Hannon Hill Cascade CMS"
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.12" # uses `Self`, `TypeAlias`, PEP 604 unions
12
12
  authors = [{ name = "Keith Shark" }]
13
13
  dependencies = [
14
- "aiohttp-client-cache[sqlite]",
15
- "python-dotenv",
14
+ "aiohttp>=3.14.3",
16
15
  "pydantic>=2",
17
16
  ]
18
17
 
@@ -6,8 +6,8 @@ payload/response models, and an OperationLogger for console/logfile output.
6
6
  """
7
7
 
8
8
  from .driver import CascadeCMSRestDriver, RequestExecutor
9
- from .operation_logger import OperationLogger
10
9
  from .operations import Node, OperationChain, Operations
10
+ from .utils.operation_logger import OperationLogger
11
11
  from .wrapper import CascadeWrapperBase
12
12
 
13
13
  __all__ = [
@@ -1060,31 +1060,25 @@ class ResponseParser[T](BaseModel):
1060
1060
  """Parses a raw response body, trying `CascadeError` first and falling
1061
1061
  back to `serializer` on the expected success shape.
1062
1062
 
1063
- `_content` holds the parsed result (either a `CascadeError` or a `T`),
1064
- and `_cacheable` is set to True only when the success-path parse
1065
- succeeds, so error responses are never cached.
1063
+ `_content` holds the parsed result (either a `CascadeError` or a `T`).
1066
1064
  """
1067
1065
 
1068
1066
  model_config = ConfigDict(arbitrary_types_allowed=True)
1069
1067
 
1070
- serializer: TypeAdapter[Any] | AssetAdapter | None = None
1068
+ serializer: TypeAdapter[Any] | AssetAdapter
1071
1069
  _content: T | CascadeError | None = PrivateAttr(default=None)
1072
- _cacheable: bool = PrivateAttr(default=False)
1073
1070
 
1074
1071
  def __init__(
1075
1072
  self,
1076
1073
  raw: bytes,
1077
- serializer: TypeAdapter[Any] | AssetAdapter | None = None,
1074
+ serializer: TypeAdapter[Any] | AssetAdapter,
1078
1075
  **kwargs,
1079
1076
  ):
1080
1077
  super().__init__(serializer=serializer, **kwargs)
1081
1078
  try:
1082
1079
  self._content = CascadeError.model_validate_json(raw)
1083
1080
  except ValidationError:
1084
- if self.serializer is None:
1085
- raise RuntimeWarning("No serializer included...")
1086
1081
  self._content = self.serializer.validate_json(raw) # type: ignore[assignment]
1087
- self._cacheable = True
1088
1082
 
1089
1083
 
1090
1084
  # ----- Parser Functions -----