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.
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/.github/workflows/release.yml +1 -15
- cascade_cms_rest-3.4.0/AGENTS.md +63 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/CHANGELOG.md +44 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/PKG-INFO +69 -18
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/PUBLISHING.md +10 -17
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/README.md +67 -15
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/pyproject.toml +2 -3
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/__init__.py +1 -1
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/cmstypes.py +3 -9
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/driver.py +19 -101
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/operations.py +131 -4
- cascade_cms_rest-3.4.0/src/cascade_cms/utils/__init__.py +5 -0
- {cascade_cms_rest-3.2.2/src/cascade_cms → cascade_cms_rest-3.4.0/src/cascade_cms/utils}/operation_logger.py +54 -20
- cascade_cms_rest-3.4.0/src/cascade_cms/utils/redaction.py +16 -0
- cascade_cms_rest-3.4.0/src/cascade_cms/utils/script_notes.py +77 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/wrapper.py +68 -22
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/edit_test.py +0 -1
- cascade_cms_rest-3.4.0/tests/test_driver.py +35 -0
- cascade_cms_rest-3.4.0/tests/test_logging_exit.py +228 -0
- cascade_cms_rest-3.4.0/tests/test_non200.py +118 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/test_operation_chains.py +1 -1
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/test_operation_logger.py +1 -1
- cascade_cms_rest-3.4.0/tests/test_redaction.py +30 -0
- cascade_cms_rest-3.4.0/tests/test_result_lines.py +126 -0
- cascade_cms_rest-3.4.0/tests/test_script_log.py +153 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/test_wrapper.py +1 -1
- cascade_cms_rest-3.2.2/AGENTS.md +0 -43
- cascade_cms_rest-3.2.2/tests/test_driver.py +0 -14
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/.github/copilot-instructions.md +0 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/.github/workflows/ci.yml +0 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/.github/workflows/dependency-review.yml +0 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/.gitignore +0 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/LICENSE +0 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/failures.py +0 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/src/cascade_cms/py.typed +0 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/conftest.py +0 -0
- {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.4.0}/tests/test_cmstypes.py +0 -0
- {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:
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
136
|
-
(the default) runs in **normal mode**: a minimal console (`[INIT]`/`[RUNNING]`/`
|
|
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,
|
|
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.
|
|
77
|
+
### 6. Publishing Happens Automatically
|
|
78
78
|
|
|
79
|
-
**
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
115
|
-
(the default) runs in **normal mode**: a minimal console (`[INIT]`/`[RUNNING]`/`
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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 -----
|