cicerone-recommender 0.6.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 (98) hide show
  1. cicerone_recommender-0.6.0/CHANGELOG.md +501 -0
  2. cicerone_recommender-0.6.0/LICENSE +7 -0
  3. cicerone_recommender-0.6.0/MANIFEST.in +9 -0
  4. cicerone_recommender-0.6.0/PKG-INFO +712 -0
  5. cicerone_recommender-0.6.0/README.md +667 -0
  6. cicerone_recommender-0.6.0/pyproject.toml +86 -0
  7. cicerone_recommender-0.6.0/requirements-redis.txt +3 -0
  8. cicerone_recommender-0.6.0/requirements-sequential.txt +3 -0
  9. cicerone_recommender-0.6.0/requirements.txt +33 -0
  10. cicerone_recommender-0.6.0/setup.cfg +4 -0
  11. cicerone_recommender-0.6.0/src/cicerone/__init__.py +6 -0
  12. cicerone_recommender-0.6.0/src/cicerone/__main__.py +4 -0
  13. cicerone_recommender-0.6.0/src/cicerone/artifact.py +240 -0
  14. cicerone_recommender-0.6.0/src/cicerone/automl.py +367 -0
  15. cicerone_recommender-0.6.0/src/cicerone/blending.py +336 -0
  16. cicerone_recommender-0.6.0/src/cicerone/cli.py +215 -0
  17. cicerone_recommender-0.6.0/src/cicerone/config/__init__.py +105 -0
  18. cicerone_recommender-0.6.0/src/cicerone/config/constants.py +54 -0
  19. cicerone_recommender-0.6.0/src/cicerone/config/events.py +126 -0
  20. cicerone_recommender-0.6.0/src/cicerone/config/load.py +441 -0
  21. cicerone_recommender-0.6.0/src/cicerone/config/lock_url.py +54 -0
  22. cicerone_recommender-0.6.0/src/cicerone/config/settings.py +266 -0
  23. cicerone_recommender-0.6.0/src/cicerone/config/validation.py +94 -0
  24. cicerone_recommender-0.6.0/src/cicerone/content_fallback.py +293 -0
  25. cicerone_recommender-0.6.0/src/cicerone/dashboard.py +146 -0
  26. cicerone_recommender-0.6.0/src/cicerone/dashboard_lookup.py +147 -0
  27. cicerone_recommender-0.6.0/src/cicerone/dashboard_users.py +27 -0
  28. cicerone_recommender-0.6.0/src/cicerone/dataset.py +170 -0
  29. cicerone_recommender-0.6.0/src/cicerone/events/__init__.py +28 -0
  30. cicerone_recommender-0.6.0/src/cicerone/events/base.py +52 -0
  31. cicerone_recommender-0.6.0/src/cicerone/events/buffer.py +111 -0
  32. cicerone_recommender-0.6.0/src/cicerone/events/db.py +372 -0
  33. cicerone_recommender-0.6.0/src/cicerone/events/ha.py +23 -0
  34. cicerone_recommender-0.6.0/src/cicerone/events/normalize.py +116 -0
  35. cicerone_recommender-0.6.0/src/cicerone/events/redis_streams.py +341 -0
  36. cicerone_recommender-0.6.0/src/cicerone/events/registry.py +43 -0
  37. cicerone_recommender-0.6.0/src/cicerone/events/s3.py +613 -0
  38. cicerone_recommender-0.6.0/src/cicerone/events/store.py +220 -0
  39. cicerone_recommender-0.6.0/src/cicerone/events/updater.py +377 -0
  40. cicerone_recommender-0.6.0/src/cicerone/events/webhook.py +106 -0
  41. cicerone_recommender-0.6.0/src/cicerone/events/worker.py +223 -0
  42. cicerone_recommender-0.6.0/src/cicerone/export_serve_openapi.py +64 -0
  43. cicerone_recommender-0.6.0/src/cicerone/feature_config.py +229 -0
  44. cicerone_recommender-0.6.0/src/cicerone/http_auth.py +48 -0
  45. cicerone_recommender-0.6.0/src/cicerone/ids.py +52 -0
  46. cicerone_recommender-0.6.0/src/cicerone/io/__init__.py +0 -0
  47. cicerone_recommender-0.6.0/src/cicerone/io/base.py +103 -0
  48. cicerone_recommender-0.6.0/src/cicerone/io/dataset_store.py +134 -0
  49. cicerone_recommender-0.6.0/src/cicerone/io/db_errors.py +19 -0
  50. cicerone_recommender-0.6.0/src/cicerone/io/db_store.py +232 -0
  51. cicerone_recommender-0.6.0/src/cicerone/io/factory.py +64 -0
  52. cicerone_recommender-0.6.0/src/cicerone/io/manifest_reader.py +85 -0
  53. cicerone_recommender-0.6.0/src/cicerone/io/options.py +122 -0
  54. cicerone_recommender-0.6.0/src/cicerone/io/recommendation_reader.py +389 -0
  55. cicerone_recommender-0.6.0/src/cicerone/io/recommendation_schema.py +38 -0
  56. cicerone_recommender-0.6.0/src/cicerone/io/replace_users.py +32 -0
  57. cicerone_recommender-0.6.0/src/cicerone/job.py +226 -0
  58. cicerone_recommender-0.6.0/src/cicerone/locks.py +355 -0
  59. cicerone_recommender-0.6.0/src/cicerone/manage_dashboard_users.py +81 -0
  60. cicerone_recommender-0.6.0/src/cicerone/model/__init__.py +50 -0
  61. cicerone_recommender-0.6.0/src/cicerone/model/combine.py +46 -0
  62. cicerone_recommender-0.6.0/src/cicerone/model/constants.py +29 -0
  63. cicerone_recommender-0.6.0/src/cicerone/model/epoch_metrics.py +135 -0
  64. cicerone_recommender-0.6.0/src/cicerone/model/fit.py +306 -0
  65. cicerone_recommender-0.6.0/src/cicerone/model/recommend.py +478 -0
  66. cicerone_recommender-0.6.0/src/cicerone/model/strategies.py +134 -0
  67. cicerone_recommender-0.6.0/src/cicerone/model_config.py +266 -0
  68. cicerone_recommender-0.6.0/src/cicerone/packaging.py +117 -0
  69. cicerone_recommender-0.6.0/src/cicerone/policy.py +367 -0
  70. cicerone_recommender-0.6.0/src/cicerone/scheduler.py +89 -0
  71. cicerone_recommender-0.6.0/src/cicerone/serve/__init__.py +21 -0
  72. cicerone_recommender-0.6.0/src/cicerone/serve/__main__.py +4 -0
  73. cicerone_recommender-0.6.0/src/cicerone/serve/app.py +468 -0
  74. cicerone_recommender-0.6.0/src/cicerone/serve/bootstrap_events.py +163 -0
  75. cicerone_recommender-0.6.0/src/cicerone/serve/code_samples.py +328 -0
  76. cicerone_recommender-0.6.0/src/cicerone/serve/events_routes.py +155 -0
  77. cicerone_recommender-0.6.0/src/cicerone/serve/metrics.py +195 -0
  78. cicerone_recommender-0.6.0/src/cicerone/serve/python_detect.sh +4 -0
  79. cicerone_recommender-0.6.0/src/cicerone/serve_client.py +92 -0
  80. cicerone_recommender-0.6.0/src/cicerone/serve_schemas.py +81 -0
  81. cicerone_recommender-0.6.0/src/cicerone/static/cicerone-icon.svg +37 -0
  82. cicerone_recommender-0.6.0/src/cicerone/static/cicerone-logo.svg +66 -0
  83. cicerone_recommender-0.6.0/src/cicerone/static/dashboard.js +64 -0
  84. cicerone_recommender-0.6.0/src/cicerone/static/tailwind.css +2 -0
  85. cicerone_recommender-0.6.0/src/cicerone/static/tailwind.input.css +10 -0
  86. cicerone_recommender-0.6.0/src/cicerone/static/vendor/htmx.min.js +1 -0
  87. cicerone_recommender-0.6.0/src/cicerone/static/vendor/stimulus.umd.js +2588 -0
  88. cicerone_recommender-0.6.0/src/cicerone/templates/_recommendations.html +45 -0
  89. cicerone_recommender-0.6.0/src/cicerone/templates/_status.html +117 -0
  90. cicerone_recommender-0.6.0/src/cicerone/templates/dashboard.html +79 -0
  91. cicerone_recommender-0.6.0/src/cicerone/trigger.py +137 -0
  92. cicerone_recommender-0.6.0/src/cicerone/values.py +50 -0
  93. cicerone_recommender-0.6.0/src/cicerone_recommender.egg-info/PKG-INFO +712 -0
  94. cicerone_recommender-0.6.0/src/cicerone_recommender.egg-info/SOURCES.txt +96 -0
  95. cicerone_recommender-0.6.0/src/cicerone_recommender.egg-info/dependency_links.txt +1 -0
  96. cicerone_recommender-0.6.0/src/cicerone_recommender.egg-info/entry_points.txt +2 -0
  97. cicerone_recommender-0.6.0/src/cicerone_recommender.egg-info/requires.txt +23 -0
  98. cicerone_recommender-0.6.0/src/cicerone_recommender.egg-info/top_level.txt +1 -0
@@ -0,0 +1,501 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.6.0] - 2026-08-20
10
+
11
+ ### Added
12
+
13
+ - Docs site homepage: latest dated CHANGELOG release and a GitHub **what changed**
14
+ link to that section.
15
+ - Dashboard user lookup: inspect a `user_id`'s current precomputed top-K
16
+ (rank, item, score, source, optional category) from the job output store,
17
+ with cold-start fallback, on the Basic-Auth status page.
18
+ `GET /dashboard?user_id=` fills the lookup on load.
19
+ - PyPI distribution `cicerone-recommender` (`import cicerone`; the name
20
+ `cicerone` is taken). Wheel includes compiled dashboard CSS. A GitHub
21
+ Release publishes via trusted publishing (`.github/workflows/publish.yml`).
22
+ - `cicerone` CLI (`start`/`job`/`serve`/`dashboard`/`scheduler`/`users` /
23
+ `export-openapi`) with `--config` for a TOML path, plus `--log-level` /
24
+ `--log-format` (or `CICERONE_LOG_LEVEL` / `CICERONE_LOG_FORMAT`). Runtime
25
+ image pip-installs the wheel; entrypoint is `cicerone start`.
26
+
27
+ - Optional project-site articles at `/articles/` (static Markdown under
28
+ `website/src/content/docs/articles/`). No nav, RSS, or index until a
29
+ published post exists. Article pages use IBM Plex Serif and a ~65ch
30
+ measure. Brand accents invert for dark theme. Listing keeps an h1;
31
+ posts use `description` for meta. Website-only PRs skip Docker lint/test
32
+ jobs; the `ci` job still succeeds.
33
+
34
+ - Optional **sequential** strategy (`SASRecModel` / `BERT4RecModel`) via
35
+ `[model.sequential]` (`architecture = "sasrec"` or `"bert4rec"`). Requires
36
+ `cicerone-recommender[sequential]` / `requirements-sequential.txt`
37
+ (`rectools[torch]`); serve mode never imports torch. AutoML drops it from
38
+ the candidate pool when the extra is missing or median distinct items/user
39
+ is below `[job.sequential].min_median_interactions` (default 5), and logs
40
+ the skip.
41
+
42
+ - Incremental events horizontal HA: leader-only apply lease
43
+ (`{lock_key}:events:apply`) when `events.ha = true` with
44
+ `job.trigger.lock_backend` postgres/redis. Fan-out sources acquire the
45
+ lease only when a micro-batch is ready. Metrics:
46
+ `cicerone_events_lock_total`, `cicerone_events_leader`,
47
+ `cicerone_events_apply_busy_total`.
48
+
49
+ - Redis Streams EventSource (`events.kind = "redis_streams"`): consumer-group
50
+ poll via `XREADGROUP` / `XACK`, idle PEL recovery with `XAUTOCLAIM`, and
51
+ stream entry id fallback when `event_id` is omitted. Requires
52
+ `cicerone-recommender[redis]` / `requirements-redis.txt` (same optional
53
+ `redis` pin as the lock backend).
54
+ - User-scoped incremental write-through: load/replace only affected users
55
+ (plus `__cold_start__`) via `OutputSink.replace_recommendations_for_users`
56
+ (returns post-write distinct user count) instead of full-frame overwrite.
57
+ Updater keeps an LRU-bounded per-user cache (default 2048); dataset
58
+ `count_recommendation_users` projects only `user_id` from parquet, and
59
+ `load_recommendations_for_users` uses parquet `filters` for `user_id` when
60
+ the engine supports predicate pushdown.
61
+ - Incremental events Prometheus metrics on serve `/metrics` (source lag /
62
+ connected, flush counters, last success timestamp, tick errors) and an
63
+ incremental-events panel on the Basic-Auth dashboard (from manifests).
64
+ - Incremental events between full retrains: internal `EventSource` surface,
65
+ webhook `POST /events`, micro-batch buffer/worker, and write-through
66
+ updater for popular/latest slices (`[events]` config). Operator guide:
67
+ `docs/incremental-events.md`. Webhook `occurred_at` requires an explicit
68
+ timezone (`Z` / offset) or Unix epoch seconds (UTC).
69
+ - DB event source (`events.kind = "db"`): watermark poll over
70
+ `events_table` / `events_query`, durable optional `watermark_path`,
71
+ watermark advances only on successful flush ack.
72
+ - S3-compatible event source (`events.kind = "s3"`), R2-first: list/marker
73
+ poll via the same `build_s3_client` / `endpoint_url` options as dataset
74
+ I/O; optional AWS-only SQS mode (rejected with `endpoint_url`). JSON
75
+ object/array payloads; ack advances marker or deletes the SQS message.
76
+
77
+ ### Changed
78
+
79
+ - Docs: `docs/how-it-works.md` (pipeline, strategies, papers); incremental
80
+ events operator guide; tutorial webhook step; site sidebar/homepage
81
+ cards; OpenAPI `x-codeSamples` for `POST /events`; PyPI extras
82
+ (`cicerone-recommender[sequential]` / `[redis]`) next to `requirements-*.txt`;
83
+ example TOML / OpenAPI regenerate command for pip hosts; missing-package
84
+ errors name the PyPI extras; operator ingest recap lives in
85
+ `docs/incremental-events.md` (other pages link it).
86
+ - Parse article `draft` from YAML frontmatter; article layout CSS keys off
87
+ `data-cicerone-articles` rather than starlight-blog class names.
88
+ - Share the articles URL prefix between the Starlight plugin and layout
89
+ classifier; `robots.txt` allows the site and disallows `/pagefind/`.
90
+ - Article plugin gating passes `{ production }` explicitly; layout kind
91
+ matches starlight-blog listing routes and ignores a missing route id.
92
+ - Drop the Starlight “Edit page” footer; site content is edited in git.
93
+ - Docker `package` stage validates the wheel via `python -m cicerone.packaging`
94
+ (selects `cicerone_recommender-<version>` including PEP 440 local versions
95
+ and numeric wheel build tags).
96
+
97
+ - README and docs-site dashboard screenshot include the user recommendation lookup.
98
+ - Docs site copies `docs/images/` into `website/public/images/docs/` at build time.
99
+ - Dashboard lookup form is labeled, results are announced, and job-run
100
+ tables expose captions / column headers; helper text contrast is higher.
101
+ - Dashboard inspector k is `min(job.top_k, dashboard.lookup_k)` (default 20).
102
+
103
+ - Bump `pyarrow` 25.0.0 → 25.0.1 (#85), `SQLAlchemy` 2.0.51 → 2.0.52 (#86),
104
+ `uvicorn` 0.52.1 → 0.52.3 (#87), `ruff` 0.16.2 → 0.16.3 (#88).
105
+ - Bump GitHub Actions Pages deploy helpers: `actions/upload-pages-artifact`
106
+ v3 → v5 (Dependabot #79), `actions/deploy-pages` v4 → v5 (#80).
107
+ - Bump `fastapi` 0.140.13 → 0.141.1 (Dependabot #69).
108
+ - Dependabot: ignore `numpy` major bumps (Python 3.11 CI) and `boto3>=1.43.57`
109
+ (aiobotocore botocore pin).
110
+ - Project docs site (not part of the runtime product): Starlight under
111
+ `website/`, synced from `docs/`, published at [cicerone.dev](https://cicerone.dev).
112
+
113
+ ### Fixed
114
+
115
+ - OpenAPI `POST /events` curl sample JSON-encodes `USER_ID` with `python3`
116
+ (falls back to `python`, then `jq`) and errors if none is on PATH.
117
+ `examples/serve/curl_examples.sh` sources the same
118
+ `src/cicerone/serve/python_detect.sh` snippet the OpenAPI samples embed.
119
+ - Docker test image includes `examples/serve/` so CI can read `curl_examples.sh`.
120
+ - Tutorial webhook step starts serve with `cicerone --config … serve` (same as
121
+ the HTTP API step).
122
+ - `cicerone users` with a config path requires enabled `dashboard.users_path`
123
+ (or an explicit `--users-path`); the error names the loaded config and the
124
+ dashboard settings that were resolved.
125
+
126
+ - Dashboard still starts if the recommendation store cannot be opened (lookup disabled).
127
+ - Dashboard lookup errors show a generic message; details stay in the logs.
128
+ - Dashboard lookup URL updates keep the hash fragment.
129
+ - Dashboard lookup disables the Look up button during the htmx request.
130
+ - Postgres `is_locked()` logs and re-raises probe failures instead of
131
+ treating a dead database as “lock free”; `owned()` logs before fail-closed.
132
+ - S3 EventSource `nack` returns events to the local pending queue (and
133
+ extends SQS visibility) instead of dropping the batch. SQS HA lock-busy
134
+ nacks can retry immediately; list-mode array payloads no longer lose
135
+ sibling events when one id is nacked.
136
+ - Event worker ack/nack bookkeeping: buffer duplicates are acked (not left
137
+ in-flight), capacity overflow is nacked for redelivery, and stop drains
138
+ the buffer once before closing the source.
139
+ - DB event source poll uses `(occurred_at, event_id)` cursor/order when an
140
+ `event_id` column exists so same-timestamp pages cannot skip rows.
141
+
142
+ ## [0.5.1] - 2026-08-12
143
+
144
+ ### Added
145
+
146
+ - Serve OpenAPI / ReDoc ``x-codeSamples`` (Ruby, Python, JavaScript, Shell)
147
+ for `/health` and `/recommendations/{user_id}`.
148
+
149
+ ### Fixed
150
+
151
+ - Redis lock `release()` joins the refresher (≤250ms) and ignores in-flight
152
+ refresh failures after an intentional stop, avoiding a `_mark_lost` race.
153
+ - Retrain Prometheus labels use the real trigger source (`cron`, `s3-poll`,
154
+ `webhook`, …) instead of collapsing non-webhook to `poll`.
155
+ - Serve fails closed when `features.toml` cannot be loaded (no silent disable
156
+ of availability filters).
157
+ - Input poller treats local `stat` errors like S3 failures (log and continue).
158
+
159
+ ### Changed
160
+
161
+ - Blending warm path uses per-user / shared latest rankings without building
162
+ Cartesian U×K latest frames; `latest_by_user` keys are normalized to `str`.
163
+ - Serve caches `generated_at` with the refresh loop; cold-start prefers the
164
+ per-user index / refresh-time fallback; category filters use a refresh-time
165
+ `category → item_id` map.
166
+ - Content-fallback fit avoids `iterrows`; recommend uses vectorized top-K.
167
+ - Event caps apply in one sort across all capped event types.
168
+ - Shared helpers: `cicerone.values` (`is_missing` / `as_list`),
169
+ `io.options.read_parquet` / `validate_storage_options`, and
170
+ `MISSING_TABLE_ERRORS`.
171
+ - `cicerone.serve` package `__all__` exports only the public API.
172
+
173
+ ## [0.5.0] - 2026-08-11
174
+
175
+ ### Added
176
+
177
+ - Serve-mode Prometheus metrics at `GET /metrics` (`prometheus-client`):
178
+ request volume/latency, cache hit/miss/age/refresh, recommendation
179
+ source tiers, retrain-trigger counts, and `cicerone_up`. Configured via
180
+ `[serve].metrics_enabled` (default `true`) and optional
181
+ `[serve].metrics_token` (`X-Metrics-Token` header; empty = open endpoint).
182
+ Per-replica registry; no multiprocess mode.
183
+ - Optional `[model.<strategy>]` TOML tables for RecTools `model_from_config`
184
+ (`collaborative`, `item_based`, `popular`, `latest`).
185
+ - Optional scheduler lock backends (`[job.trigger].lock_backend`:
186
+ `in_process` default, `postgres`, `redis`) for multi-replica mutual
187
+ exclusion; manifest records `lock_backend`. Redis is an optional
188
+ install (`requirements-redis.txt`). Optional `lock_key` /
189
+ `lock_ttl_seconds` that namespace shared Redis/Postgres lock stores.
190
+ Redis lock TTL is refreshed while held so long runs stay exclusive.
191
+
192
+ ### Changed
193
+
194
+ - Strategies built via RecTools `model_from_config` / `get_config`.
195
+ - Artifacts schema **v3**: RecTools `model.save` / `load_model` in a zip
196
+ envelope; `content_fallback` still pickle. Schema v2 no longer loadable.
197
+ `created_at` is stored as ISO-8601 in `meta.json` and loaded as `datetime`.
198
+ - Split `model` / `config` into packages; import paths unchanged. Cross-module
199
+ helpers are public (`_` = module-local only). Tests follow the same split
200
+ (`test_model_*.py`, `test_config_*.py`).
201
+ - Docs: architecture test/module map, tutorial `[model.*]` knobs, README /
202
+ CONTRIBUTING test layout notes; serve `/metrics` in README, architecture,
203
+ tutorial, and `examples/serve/`.
204
+ - `cicerone.serve` is a package (`serve/app.py`, `serve/metrics.py`);
205
+ `python -m cicerone.serve` and `from cicerone.serve import create_app`
206
+ unchanged.
207
+ - Shared defaults: item-based `K` from `DEFAULT_ITEM_BASED_K_NEIGHBORS`;
208
+ latest window from `model_config.LATEST_WINDOW_DAYS` (re-exported by
209
+ `model.constants`).
210
+
211
+ ### Renamed config keys (legacy still accepted)
212
+
213
+ - `job.item_based.k_neighbors` → `model.item_based.model.K`. Conflicting
214
+ values raise `ConfigError`.
215
+
216
+ ## [0.4.1] - 2026-08-07
217
+
218
+ ### Added
219
+
220
+ - Nested settings surfaces on `Settings`: `serve`, `trigger`, `dashboard`, and
221
+ `automl` dataclasses (flat `settings.serve_host`-style accessors remain as
222
+ compatibility properties). Invalid config knobs raise `ConfigError`
223
+ (`ValueError` subclass); missing files / unset `${ENV}` still raise
224
+ `RuntimeError`.
225
+ - Manifest field `partial_outputs` when a sink write fails after some outputs
226
+ were already persisted (success is set only after all writes succeed).
227
+ - `[[boosts]]` accepted as an alias for `[[boost]]` in `features.toml`
228
+ (defining both is an error).
229
+ - I/O factory kind registry; `configure_item_filters` on the
230
+ `RecommendationReader` protocol; frozen `ModelArtifact`.
231
+
232
+ ### Changed
233
+
234
+ - Event caps keep the **most recent** N events per `(user, item, event_type)`
235
+ (sorted by `occurred_at` descending before `cumcount`).
236
+ - Aggregated `(user, item)` pairs with non-positive weight are **dropped**
237
+ instead of being floored to `1e-3` (negative review sums no longer become
238
+ weak positive LightFM signals).
239
+ - Serve cold-start heuristic is deterministic across dataset and DB backends:
240
+ prefer `popular_fallback`, then `latest`, then lexicographic `user_id`;
241
+ DB path picks one user then fetches that user's top-K (no `LIMIT k*20`
242
+ under-fill).
243
+ - `recommend_with_models` split into cohort → recommend → combine → boost
244
+ phases; blending uses per-user indexes and optional shared latest rankings;
245
+ serve dataset reader indexes recommendations by `user_id` at refresh time.
246
+ - ProcessPool strategy now initializes LightFM with `num_threads=1` inside
247
+ workers to avoid CPU oversubscription.
248
+
249
+ ### Fixed
250
+
251
+ - `item_true` eligibility no longer treats non-empty strings such as
252
+ `"false"` / `"0"` as true (`astype(bool)`); only explicit truthy tokens
253
+ / non-zero numerics / bools pass.
254
+ - DB `_clear_table_for_replace` catches the same missing-table errors as the
255
+ rest of the DB store (including SQLite `OperationalError` on `TRUNCATE`).
256
+
257
+ ## [0.4.0] - 2026-08-06
258
+
259
+ ### Added
260
+
261
+ - **Content cold-item fallback** (`content_fallback` strategy): recommends
262
+ zero-interaction items by one-hot cosine similarity over configured
263
+ `item_features` against each warm user's history. Gated by
264
+ `[job.content_fallback].enabled` (default off); when enabled, auto-inserted
265
+ before the first non-personalized strategy. Independent of `item_based`.
266
+ Source label: `content_fallback`. Requires `scikit-learn`.
267
+ - **Weighted multi-source blending** (`[blending]` in `config/features.toml`):
268
+ replaces the binary personalized-vs-`popular_fallback` choice with a
269
+ gradual per-user mix of `personalized`, `popular`, and date-based
270
+ `latest` (publication/`occurred_at`-style columns on `items`). A
271
+ configurable sigmoid or linear curve maps interaction count →
272
+ personalized weight; the remainder is split by `popular_share`. When no
273
+ usable date column exists, `latest` is disabled and its weight moves to
274
+ `popular`. Combined rows use `source = "blended"`. Availability /
275
+ eligibility still filter every source before the blend. A shared
276
+ `__cold_start__` row set is written for serve-mode fallback.
277
+ - **Serve read contract** aligned with a Gorse-style lookup (without
278
+ adopting Gorse infra): `GET /recommendations/{user_id}` accepts
279
+ `limit` / `k`, `category`, and `exclude_unavailable`, returns
280
+ `{generated_at, user_id, fallback, items:[{item_id,rank,score,source}]}`
281
+ plus an `X-Generated-At` header from the last run manifest, and falls
282
+ back to the precomputed cold-start list for unknown users (not a bare
283
+ 404). Items are snapshotted to the output store
284
+ (`items_snapshot.parquet` / `recommendation_items`) so filters stay on
285
+ the configured output without loading ML deps. Documented in the README
286
+ Serve section alongside the existing Dashboard style.
287
+ - **Serve OpenAPI contract**: response models for `/health` and
288
+ `/recommendations/{user_id}` so FastAPI's `/openapi.json`, `/docs`, and
289
+ `/redoc` document the real JSON shape (including `X-Generated-At`). A
290
+ checked-in schema at `docs/openapi/serve.openapi.json` can be regenerated
291
+ with `python -m cicerone.export_serve_openapi`.
292
+ - **Thin serve clients**: `cicerone.serve_client.ServeClient` (stdlib
293
+ `urllib`, typed via `serve_schemas`) plus copy-paste examples under
294
+ `examples/serve/` (Python, Node `fetch`, curl).
295
+
296
+ ### Changed
297
+
298
+ - **Default model chain** is now `["collaborative", "item_based", "popular"]`
299
+ (was `["collaborative", "popular"]`), so sparse warm users get item-KNN
300
+ backfill before raw popularity.
301
+ - **`[job.item_based].k_neighbors`** configures `TFIDFRecommender(K=…)`
302
+ (default `20`).
303
+ - Serve JSON response is an object (with `generated_at` / `items`) rather
304
+ than a bare list; `k` remains accepted as an alias for `limit`.
305
+ - Docs: architecture data-flow covers the three combine paths (priority /
306
+ RRF / `[blending]`) and the items snapshot write; tutorial gains a
307
+ blending walkthrough and an updated serve-API section (response shape,
308
+ `limit` / `category` / cold-start fallback, OpenAPI `/docs`,
309
+ `examples/serve/` clients).
310
+ - Blending correctness: date-based `latest` is ranked per eligibility
311
+ cohort (no cross-cohort allowlist union); `__cold_start__` uses the
312
+ global item-scoped allowlist; multi-personalized strategies collapse to
313
+ best rank before RRF; sigmoid maps `n=0 → 0`; strategy `latest` is
314
+ skipped while blending is on; serve heuristic fallback never reuses
315
+ warm `blended` rows.
316
+ - `cicerone.serve.main` imports I/O factory helpers lazily so OpenAPI export
317
+ and `create_app` do not require dataset/DB backend imports at module load.
318
+ - Serve OpenAPI `info.version` follows `cicerone.__version__` (single source
319
+ of truth with the changelog / release tag).
320
+ - `config.make_settings(**overrides)` is the shared Settings factory for
321
+ tests and OpenAPI schema export (replacing duplicated default blocks).
322
+
323
+ ## [0.3.2] - 2026-08-04
324
+
325
+ ### Added
326
+
327
+ - Optional per-epoch LightFM training metrics (`[job].log_epoch_metrics`,
328
+ default `false`; interval via `[job].epoch_metrics_every`, default `5`):
329
+ when enabled, the collaborative strategy fits via `fit_partial` one epoch
330
+ at a time and logs in-sample Precision@K/Recall@K over a seeded random
331
+ user sample. Tunables (`epoch_metrics_max_users`, regression/plateau
332
+ thresholds) live on `EpochMetricsSettings`. Significant regression or late
333
+ plateau across logged epochs emits a WARN. Off by default so scheduled
334
+ batch runs stay unchanged.
335
+ - Top-K ID-mapping regression test: synthetic catalog with sparse external
336
+ IDs that must not collide with rectools' dense internal indices, asserting
337
+ no duplicate items per user and no seen items in personalized rows.
338
+
339
+ ### Changed
340
+
341
+ - Documented that per-strategy top-K extraction is rectools-native
342
+ (`ModelBase.recommend()` + Dataset id maps) and that AutoML eval metrics
343
+ already use `rectools.metrics.calc_metrics` (MAP/NDCG/Recall) — no custom
344
+ metric implementations.
345
+
346
+ ## [0.3.1] - 2026-08-04
347
+
348
+ ### Changed
349
+
350
+ - README gains a top-level **Features** list (batch, strategies, AutoML,
351
+ policies, serve, trigger, dashboard, artifacts).
352
+
353
+ - Priority combine fills top-K from earlier strategies first (stable
354
+ priority + rank sort) instead of interleaving by per-strategy rank.
355
+ - `[job].max_workers` drives ProcessPool parallelism for AutoML folds and
356
+ strategy fitting (default `1` / sequential; set `>1` to opt in).
357
+ - AutoML `primary_metric` matches an exact name or a single `NAME@k`;
358
+ ambiguous `NAME@k` sets are rejected (no order-dependent first match).
359
+ - Shared I/O helpers: `object_key`, `sql_identifier`, S3 not-found checks.
360
+ - Model-artifact DB writes use portable SQLAlchemy `LargeBinary` /
361
+ timezone-aware `DateTime` and clear via `DELETE` (dialect-agnostic).
362
+ - Config validates `top_k`, `half_life_days`, feature column types, and
363
+ `model_weights` keys against `job.models`.
364
+ - Batch job builds artifacts in memory and writes outputs only after
365
+ successful compute, reducing partial failed-run publishes.
366
+
367
+ ### Fixed
368
+
369
+ - Optional dataset `users`/`items` reads only treat missing files / S3
370
+ not-found as absent; other errors propagate.
371
+
372
+ ## [0.3.0] - 2026-08-03
373
+
374
+ ### Added
375
+
376
+ - **Business policy layer** (`config/features.toml` → `[[eligibility]]` /
377
+ `[[boost]]`): declarative hard filters (region/nationality, market match,
378
+ category allowlists) and soft ranking boosts (paying producers, plan
379
+ tiers, numeric lifts). Applied at batch recommend time via
380
+ `cicerone.policy`; serve mode stays a lookup of already-policy-aware rows.
381
+ `item_availability_filters` remains sugar for global `item_true` rules.
382
+ User-scoped eligibility groups users into cohorts whose allowed-item set
383
+ is computed once and reused across strategies; missing `item_column`
384
+ warnings are deduplicated per `(kind, rule, column)`. When boosts are
385
+ configured, candidates are over-fetched (`boost_overfetch_factor` ×
386
+ `top_k`, default 3 — tunable in `features.toml`) before score multipliers
387
+ so a commercially boosted item just outside the raw top-K can still enter
388
+ the final list. Cohorts whose eligibility excludes every item get an empty
389
+ allowlist (no silent catalog fallback) and are skipped.
390
+ - Optional **model artifact** (`[job].save_model_artifact`): the batch job
391
+ can write a versioned, portable fitted-model bundle
392
+ (`model.artifact` / `model_artifacts` table) alongside recommendations.
393
+ Load and recommend without re-fitting via `cicerone.artifact`. Serve mode
394
+ still reads precomputed rows only — this does not add live inference.
395
+ Artifacts persist the `users` frame (schema version **2**) so
396
+ `recommend_from_artifact` can re-apply user-scoped eligibility offline.
397
+ - Optional `postgres` service in `docker-compose.yml` (Postgres 16, compose
398
+ profile `db`) for local `kind = "db"` input/output. First boot creates
399
+ `cicerone` (app/tutorial) and `cicerone_test` (pytest) databases.
400
+ `INPUT_DATABASE_URL` / `OUTPUT_DATABASE_URL` stay unset unless provided
401
+ via `.env`.
402
+ - System-style end-to-end test (`tests/test_system_db.py`) against a real
403
+ Postgres: seeds shared conftest fixtures → `job.run` (db in/out + model
404
+ artifact) → verify recommendations, manifest, artifact blob, and the
405
+ serve/dashboard DB readers. Schema reset is module-scoped, reflects via
406
+ SQLAlchemy metadata, and is gated by a test DB name check plus
407
+ `ALLOW_SCHEMA_RESET_FOR_TESTS=1`.
408
+ - User-facing documentation for model artifacts and business policies:
409
+ README sections, tutorial §8–9, architecture notes, annotated recipes in
410
+ `config/features.toml`, and `model_artifact_table` in the commented db
411
+ config example.
412
+
413
+ ### Changed
414
+
415
+ - `model.train_and_recommend` is split into `fit_strategies` +
416
+ `recommend_with_models` so fitted weights can be reused (AutoML cache,
417
+ model artifacts) without a second fit.
418
+ - Tutorial database section uses
419
+ `docker compose --profile db up -d postgres` instead of an ad-hoc
420
+ `docker run` Postgres container.
421
+ - Local pytest guidance points `TEST_DATABASE_URL` at `cicerone_test` (with
422
+ `ALLOW_SCHEMA_RESET_FOR_TESTS=1`) so tests do not wipe app data.
423
+
424
+ ### Fixed
425
+
426
+ - Model-artifact DB writes use `CREATE TABLE IF NOT EXISTS` + `TRUNCATE` +
427
+ `INSERT` (no `DROP`/`CREATE` race under concurrent jobs).
428
+ - `model_artifact_table` is validated as a simple SQL identifier before
429
+ interpolation.
430
+ - `cicerone.artifact` documents that pickle loads are trusted-internal-only
431
+ (never user-controlled payloads; not on the serve path).
432
+
433
+ ## [0.2.1] - 2026-07-29
434
+
435
+ ### Added
436
+
437
+ - AutoML candidate backtesting can now evaluate time-based folds in
438
+ parallel: `evaluate_candidates(..., max_workers=1)` runs folds through a
439
+ `ProcessPoolExecutor` when `max_workers > 1`, instead of sequentially
440
+ (opt-in; default behavior/performance is unchanged).
441
+ - Strategy fitting in `train_and_recommend(..., max_workers=1)` can
442
+ likewise fit independent, not-yet-cached strategies via a
443
+ `ProcessPoolExecutor` when `max_workers > 1` (opt-in; default is
444
+ unchanged).
445
+
446
+ ### Changed
447
+
448
+ - The job's input reads (events, users, items) now run concurrently via a
449
+ `ThreadPoolExecutor` instead of sequentially, speeding up runs against
450
+ network-backed (S3) input sources.
451
+
452
+ ## [0.2.0] - 2026-07-29
453
+
454
+ ### Added
455
+
456
+ - **Serve mode**: an optional lightweight, read-only HTTP API
457
+ (`[job].mode = "serve"`) that exposes precomputed recommendations for a
458
+ given user without any model loaded in the request path. Protected by a
459
+ bearer token (`[serve].auth_token` / `SERVE_AUTH_TOKEN`).
460
+ - **Event-driven retrain trigger**: an opt-in webhook
461
+ (`[job.trigger].enabled = true`) that lets an external system request an
462
+ immediate retrain instead of waiting for the next scheduled run, with an
463
+ optional S3-poll mode and a debounce guard against duplicate/overlapping
464
+ triggers. Protected by a bearer token (`[job.trigger].auth_token` /
465
+ `TRIGGER_AUTH_TOKEN`).
466
+ - **Dashboard**: a small, standalone read-only status page
467
+ (`[dashboard].enabled = true`) showing recent job run history and errors,
468
+ independent of `[job].mode`. Protected by HTTP Basic Auth against a set of
469
+ named users managed via the new `manage_dashboard_users` CLI. Built with
470
+ htmx + Stimulus + Tailwind, polling a `/partials/status` endpoint.
471
+ - `docs/tutorial.md`: new walkthroughs for serve mode, the retrain trigger,
472
+ and the dashboard.
473
+
474
+ ### Changed
475
+
476
+ - README's Security section corrected: previously claimed the container
477
+ "accepts no inbound connections" unconditionally; now documents that the
478
+ serve API, trigger webhook, and dashboard each expose one port only when
479
+ explicitly enabled, and describes how each is protected.
480
+ - `.env.example` now documents `SERVE_AUTH_TOKEN` and `TRIGGER_AUTH_TOKEN`.
481
+ - `config/cicerone.toml` now includes a commented `[dashboard]` example
482
+ alongside the existing `[serve]` and `[job.trigger]` examples.
483
+
484
+ ## [0.1.0] - 2026-07-22
485
+
486
+ ### Added
487
+
488
+ - Initial batch recommender job: reads interaction events, trains a hybrid
489
+ [rectools](https://github.com/MobileTeleSystems/RecTools) + LightFM model,
490
+ and writes top-K recommendations per user.
491
+ - Pluggable input/output backends: parquet files (`dataset`, S3-compatible
492
+ or local disk) and SQLAlchemy-backed database tables (`db`).
493
+ - Pluggable multi-model strategy registry (`collaborative`, `item_based`,
494
+ `popular`, `latest`) with weighted reciprocal rank fusion across models.
495
+ - AutoML mode to automatically pick a model strategy.
496
+ - Generic, TOML-based configuration (`config/cicerone.toml`,
497
+ `config/features.toml`) with `${ENV_VAR}` secret interpolation.
498
+ - Scheduler for running the job on a cron schedule.
499
+ - Getting-started tutorial (`docs/tutorial.md`) and architecture overview
500
+ (`docs/architecture.md`).
501
+ - CI: ruff lint/format, mypy, pip-audit, CodeQL, Dependabot.
@@ -0,0 +1,7 @@
1
+ ----------------------------------------------------------------------------
2
+ "THE BEER-WARE LICENSE" (Revision 42):
3
+ <ngw@torbido.co> wrote this project. As long as you retain this notice you
4
+ can do whatever you want with this stuff. If we meet some day, and you think
5
+ this stuff is worth it, you can buy me a beer in return — or, even better,
6
+ buy me one straight from Torbido (https://torbido.it).
7
+ ----------------------------------------------------------------------------
@@ -0,0 +1,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include requirements.txt
5
+ include requirements-redis.txt
6
+ include requirements-sequential.txt
7
+ graft src/cicerone/static
8
+ graft src/cicerone/templates
9
+ include src/cicerone/serve/python_detect.sh