cascade-cms-rest 3.2.2__tar.gz → 3.3.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 (33) hide show
  1. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/.github/workflows/release.yml +1 -15
  2. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/AGENTS.md +13 -0
  3. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/CHANGELOG.md +30 -0
  4. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/PKG-INFO +28 -18
  5. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/PUBLISHING.md +10 -17
  6. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/README.md +26 -15
  7. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/pyproject.toml +2 -3
  8. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/src/cascade_cms/cmstypes.py +3 -9
  9. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/src/cascade_cms/driver.py +18 -100
  10. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/src/cascade_cms/operation_logger.py +40 -18
  11. cascade_cms_rest-3.3.0/src/cascade_cms/redaction.py +16 -0
  12. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/src/cascade_cms/wrapper.py +56 -21
  13. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/tests/edit_test.py +0 -1
  14. cascade_cms_rest-3.3.0/tests/test_driver.py +35 -0
  15. cascade_cms_rest-3.3.0/tests/test_logging_exit.py +228 -0
  16. cascade_cms_rest-3.3.0/tests/test_non200.py +118 -0
  17. cascade_cms_rest-3.3.0/tests/test_redaction.py +30 -0
  18. cascade_cms_rest-3.2.2/tests/test_driver.py +0 -14
  19. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/.github/copilot-instructions.md +0 -0
  20. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/.github/workflows/ci.yml +0 -0
  21. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/.github/workflows/dependency-review.yml +0 -0
  22. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/.gitignore +0 -0
  23. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/LICENSE +0 -0
  24. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/src/cascade_cms/__init__.py +0 -0
  25. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/src/cascade_cms/failures.py +0 -0
  26. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/src/cascade_cms/operations.py +0 -0
  27. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/src/cascade_cms/py.typed +0 -0
  28. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/tests/conftest.py +0 -0
  29. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/tests/test_cmstypes.py +0 -0
  30. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/tests/test_operation_chains.py +0 -0
  31. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/tests/test_operation_logger.py +0 -0
  32. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/tests/test_operations.py +0 -0
  33. {cascade_cms_rest-3.2.2 → cascade_cms_rest-3.3.0}/tests/test_wrapper.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
@@ -26,6 +26,19 @@ skill-authoring workflow and how to rebuild release artifacts — both depend
26
26
  on the *published* `cascade-cms-rest` package, not a local copy, so nothing
27
27
  here needs to stay in sync with them beyond tagging a release.
28
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
+ - The API bearer token is masked (`cascade_cms.redaction.mask_token`) wherever it could surface.
41
+
29
42
  ## Maintaining the CHANGELOG
30
43
 
31
44
  Each section in `CHANGELOG.md` should include a note at the bottom listing
@@ -2,6 +2,36 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.3.0]
6
+
7
+ ### Removed
8
+ - **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.
9
+ - **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.
10
+ - **BREAKING: `backendConfig` is removed from `CascadeCMSRestDriver`.**
11
+ - **BREAKING: `python-dotenv` is no longer a dependency**, and `aiohttp-client-cache` is dropped. `aiohttp` is now a direct dependency (`>=3.14.3`).
12
+
13
+ ### Changed
14
+ - **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.
15
+ - **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.
16
+ - **BREAKING: `ResponseParser.serializer` is now required**; the `RuntimeWarning` branch is gone.
17
+ - **BREAKING: `RequestExecutor.parser` is now required** (the unused default was removed). `RequestExecutor` is exported from `cascade_cms/__init__.py`.
18
+ - 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.
19
+ - A batch-level failure's prefix follows its cause: `[NETWORK]` for network causes, `[CASCADE-REST-CMS]` otherwise.
20
+ - `CascadeCMSRestDriver.close()` clears the closed event loop as the current loop.
21
+ - 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.
22
+
23
+ ### Added
24
+ - `log_dir` keyword-only parameter on `CascadeWrapperBase` (normal and debug mode; created if missing; wins over a debug config's `log_dir`).
25
+ - `[LOG]: <path>` is printed to stderr right after `[INIT]`.
26
+ - The logfile records the tally line and a final `[EXIT-CODE]: <outcome>` line.
27
+ - `cascade_cms.redaction.mask_token()`; the debug logfile masks the `Authorization` header to its last four characters (`Bearer ****abcd`).
28
+
29
+ ### Fixed
30
+ - Each wrapper closes and releases its logfile handle on exit.
31
+ - 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.
32
+
33
+ **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`
34
+
5
35
  ## [3.2.2]
6
36
 
7
37
  ### 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.3.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,7 +160,7 @@ 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
 
@@ -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,7 +140,7 @@ 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
 
@@ -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.3.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
 
@@ -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 -----
@@ -1,16 +1,13 @@
1
1
  import asyncio
2
2
  import hashlib
3
3
  from collections.abc import Callable
4
- from dataclasses import dataclass, field
4
+ from dataclasses import dataclass
5
5
  from typing import Any, Literal, TypeVar
6
6
 
7
7
  from aiohttp import ClientResponse, ClientSession
8
- from aiohttp_client_cache import SQLiteBackend
9
-
10
- # from aiohttp_client_cache.session import CachedSession
11
- from aiohttp_client_cache.response import CachedResponse
12
8
 
13
9
  from .cmstypes import (
10
+ CascadeError,
14
11
  Payloads,
15
12
  ResponseParser,
16
13
  serialize_payload,
@@ -20,40 +17,6 @@ from .operation_logger import OperationLogger
20
17
  T = TypeVar("T")
21
18
 
22
19
 
23
- @dataclass
24
- class CacheHandler:
25
- """Thin wrapper around an aiohttp-client-cache SQLite backend.
26
-
27
- Only GET responses are ever cached (enforced by the backend's
28
- `allowed_methods` config) so repeated reads skip the network,
29
- while POST/PUT requests always hit the server.
30
- """
31
-
32
- cache_db: SQLiteBackend
33
-
34
- async def get_response(
35
- self,
36
- key: str,
37
- ) -> CachedResponse | None:
38
- return await self.cache_db.get_response(key)
39
-
40
- def get_cache_key(
41
- self,
42
- method: str,
43
- url: str,
44
- ) -> str:
45
- """Returns a response in cach if it exists"""
46
- return self.cache_db.create_key(method, url)
47
-
48
- async def save_response(
49
- self,
50
- response: ClientResponse,
51
- cache_key: str,
52
- ):
53
- """Check to see if response is cacheable"""
54
- return await self.cache_db.save_response(response, cache_key)
55
-
56
-
57
20
  @dataclass
58
21
  class RequestExecutor[T]:
59
22
  """Represents a single HTTP request and how to parse its response.
@@ -64,9 +27,7 @@ class RequestExecutor[T]:
64
27
 
65
28
  url: str
66
29
  method: Literal["GET", "POST", "PUT"]
67
- parser: Callable[..., ResponseParser[T]] = field(
68
- default=lambda raw: ResponseParser(raw=raw)
69
- )
30
+ parser: Callable[..., ResponseParser[T]]
70
31
  payload: Payloads | None = None
71
32
  identifier: Any = None
72
33
 
@@ -88,19 +49,15 @@ class RequestExecutor[T]:
88
49
  self,
89
50
  session: ClientSession,
90
51
  sem: asyncio.Semaphore,
91
- cache: CacheHandler,
92
52
  logger: "OperationLogger | None" = None,
93
53
  ) -> T:
94
- """Execute this request, using the cache for GETs when possible.
54
+ """Execute this request and parse its response.
95
55
 
96
- Checks the cache first; on a miss, performs the network request,
97
- parses the response, and stores it in the cache if parsing marked
98
- it cacheable. Concurrency across requests is bounded by `sem`.
56
+ Concurrency across requests is bounded by `sem`.
99
57
 
100
58
  Args:
101
59
  session: Shared aiohttp session to issue the request on.
102
60
  sem: Semaphore limiting concurrent in-flight requests.
103
- cache: Cache handler used to short-circuit repeated GETs.
104
61
  logger: Optional logger for recording request/response detail.
105
62
 
106
63
  Returns:
@@ -111,23 +68,18 @@ class RequestExecutor[T]:
111
68
  if self.payload:
112
69
  payload_bytes = serialize_payload(self.payload)
113
70
 
114
- # Cache the Response
115
- cache_key = cache.get_cache_key(self.method, self.url)
116
- already_cached = await cache.get_response(cache_key)
117
- if already_cached is not None:
118
- raw_data = await already_cached.read()
119
- if logger:
120
- logger.log_cache_hit(self.method, self.url)
121
- logger.write_response_file(self.log_key, raw_data)
122
- parsed_response = self.parser(raw_data)
123
- return parsed_response._content # type: ignore[return-value]
124
-
125
71
  async with session.request(
126
72
  self.method,
127
73
  self.url,
128
74
  data=payload_bytes,
129
75
  ) as response:
130
- response.raise_for_status()
76
+ if response.status != 200:
77
+ # Cascade always answers 200 with a JSON body; anything
78
+ # else is an HTML page from the Tomcat layer. Report the
79
+ # status only — the body is never read, logged or shown.
80
+ return CascadeError( # type: ignore[return-value]
81
+ message=f"{response.status} {response.reason}"
82
+ )
131
83
  raw_data = await response.read()
132
84
  if logger:
133
85
  logger.write_response_file(self.log_key, raw_data)
@@ -137,29 +89,9 @@ class RequestExecutor[T]:
137
89
  )
138
90
  # parse raw data
139
91
  parsed_response = self.parser(raw_data)
140
-
141
- if parsed_response._cacheable:
142
- await cache.save_response(
143
- response,
144
- cache_key,
145
- )
146
92
  return parsed_response._content # type: ignore[return-value]
147
93
 
148
94
 
149
- def default_cache_backend() -> SQLiteBackend:
150
- """Build the default cache backend: SQLite, GET-only, 200s only.
151
-
152
- Constructed on demand rather than at module scope so that merely
153
- importing `cascade_cms` does not create a `./cache/` directory in the
154
- caller's working directory.
155
- """
156
- return SQLiteBackend(
157
- cache_name="./cache/cache.sqlite",
158
- allowed_codes=(200,),
159
- allowed_methods=("GET",),
160
- )
161
-
162
-
163
95
  class CascadeCMSRestDriver:
164
96
  """
165
97
  URL builder and executor for Cascade CMS 8 REST API.
@@ -172,21 +104,18 @@ class CascadeCMSRestDriver:
172
104
  self,
173
105
  apiKey: str,
174
106
  cascade_url: str,
175
- backendConfig: dict[str, Any] | None,
176
107
  logger: OperationLogger | None = None,
177
108
  ):
178
- """Set up the aiohttp session, event loop, and cache for this driver.
109
+ """Set up the aiohttp session and event loop for this driver.
179
110
 
180
111
  Creates a dedicated event loop that is reused for the lifetime of
181
112
  this driver instance (rather than one per call), so all requests
182
- and cache I/O run on the same loop.
113
+ run on the same loop.
183
114
 
184
115
  Args:
185
116
  apiKey: Cascade CMS API bearer token.
186
117
  cascade_url: Base URL of the Cascade CMS instance (without
187
118
  the `/api/v1` suffix, which is appended automatically).
188
- backendConfig: kwargs forwarded to `SQLiteBackend` to override
189
- the default cache config, or None to use the default.
190
119
  logger: Optional logger for recording operations/errors.
191
120
  """
192
121
 
@@ -196,7 +125,7 @@ class CascadeCMSRestDriver:
196
125
  self.base_url = f"{cascade_url}/api/v1"
197
126
 
198
127
  # Stores response object for debugging purpose. the ClientResponse object contains request infomation as well.
199
- self.request_response_info: CachedResponse | None = None
128
+ self.request_response_info: ClientResponse | None = None
200
129
 
201
130
  # intializing event loop, for re-use for the rest of the session.
202
131
  self.eventLoop = asyncio.new_event_loop()
@@ -208,12 +137,6 @@ class CascadeCMSRestDriver:
208
137
  "Content-Type": "application/json;charset=UTF-8",
209
138
  }
210
139
 
211
- self.cache: CacheHandler
212
- if backendConfig is None:
213
- self.cache = CacheHandler(default_cache_backend())
214
- else:
215
- self.cache = CacheHandler(SQLiteBackend(**backendConfig))
216
-
217
140
  # Shared across every chain running on this driver, so concurrency
218
141
  # stays bounded no matter how many chains are in flight at once.
219
142
  self._semaphore = asyncio.Semaphore(self.MAX_REQUESTS)
@@ -259,7 +182,7 @@ class CascadeCMSRestDriver:
259
182
  # job (execute/execute_async), done after this returns a value.
260
183
  try:
261
184
  return await executor.fetch(
262
- self.session, self._semaphore, self.cache, self._logger
185
+ self.session, self._semaphore, self._logger
263
186
  )
264
187
  except Exception as exc: # noqa: BLE001 - surfaced as a value, see docstring
265
188
  return exc
@@ -283,17 +206,12 @@ class CascadeCMSRestDriver:
283
206
  return self.eventLoop.run_until_complete(self.execute_requests(requests))
284
207
 
285
208
  def close(self):
286
- """Tear down the aiohttp session, cache DB, and event loop."""
209
+ """Tear down the aiohttp session and event loop."""
287
210
  if getattr(self, "session", None) is not None and not getattr(
288
211
  self.session, "closed", False
289
212
  ):
290
213
  self.eventLoop.run_until_complete(self.session.close())
291
214
 
292
- if (
293
- getattr(self, "cache", None) is not None
294
- and getattr(self.cache, "cache_db", None) is not None
295
- ):
296
- self.eventLoop.run_until_complete(self.cache.cache_db.close())
297
-
298
215
  self.eventLoop.close()
216
+ asyncio.set_event_loop(None)
299
217
  return True