thoughtleaders-cli 0.9.0__tar.gz → 0.9.1__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 (146) hide show
  1. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/PKG-INFO +1 -1
  3. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/hooks/scripts/pre-check.sh +4 -6
  4. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/pyproject.toml +1 -1
  5. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl/SKILL.md +10 -0
  6. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl/references/business-glossary.md +1 -0
  7. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl/references/firebolt-schema.md +2 -0
  8. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl/references/postgres-schema.md +1 -0
  9. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/SKILL.md +1 -1
  10. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/references/sponsorship_filterset_schema.json +1 -1
  11. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/__init__.py +1 -1
  12. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/client/errors.py +12 -1
  13. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/db.py +56 -23
  14. thoughtleaders_cli-0.9.1/src/tl_cli/commands/profiles.py +59 -0
  15. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/main.py +2 -0
  16. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/query_history.py +69 -9
  17. thoughtleaders_cli-0.9.1/tests/test_db_repeat_warning.py +194 -0
  18. thoughtleaders_cli-0.9.1/tests/test_error_hints.py +45 -0
  19. thoughtleaders_cli-0.9.1/tests/test_profiles.py +66 -0
  20. thoughtleaders_cli-0.9.0/tests/test_db_repeat_warning.py +0 -121
  21. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/.claude-plugin/marketplace.json +0 -0
  22. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/.github/dependabot.yml +0 -0
  23. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/.github/workflows/ci.yml +0 -0
  24. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/.github/workflows/cli-integration.yml +0 -0
  25. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/.github/workflows/python-publish.yml +0 -0
  26. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/.gitignore +0 -0
  27. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/AGENTS.md +0 -0
  28. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/API.md +0 -0
  29. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/CLAUDE.md +0 -0
  30. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/LICENSE +0 -0
  31. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/README.md +0 -0
  32. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/agents/keyword-context-classifier.md +0 -0
  33. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/agents/keyword-entity-resolver.md +0 -0
  34. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/agents/keyword-relevance-validator.md +0 -0
  35. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/agents/tl-analyst.md +0 -0
  36. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/agents/youtube-comment-classifier.md +0 -0
  37. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/hooks/hooks.json +0 -0
  38. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/hooks/scripts/load-tl-skill.mjs +0 -0
  39. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/hooks/scripts/post-usage.sh +0 -0
  40. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl/references/elasticsearch-schema.md +0 -0
  41. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/.gitignore +0 -0
  42. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/SKILL.md +0 -0
  43. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
  44. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
  45. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
  46. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/references/scoring.md +0 -0
  47. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
  48. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
  49. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
  50. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
  51. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
  52. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
  53. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
  54. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/report.py +0 -0
  55. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
  56. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/score.py +0 -0
  57. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
  58. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
  59. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
  60. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-keyword-research/SKILL.md +0 -0
  61. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-keyword-research/references/elasticsearch-content-search.md +0 -0
  62. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-keyword-research/references/help.md +0 -0
  63. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-keyword-research/scripts/build_report.py +0 -0
  64. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-keyword-research/scripts/expand_entities.py +0 -0
  65. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-keyword-research/scripts/fetch_context.py +0 -0
  66. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-keyword-research/scripts/probe.py +0 -0
  67. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-keyword-research/scripts/search_channels.py +0 -0
  68. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-keyword-research/scripts/search_videos.py +0 -0
  69. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-keyword-research/scripts/select_keywords.py +0 -0
  70. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/references/columns_brands.md +0 -0
  71. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/references/columns_channels.md +0 -0
  72. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/references/columns_content.md +0 -0
  73. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
  74. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
  75. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
  76. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/references/report_glossary.md +0 -0
  77. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/references/sortable_columns.json +0 -0
  78. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
  79. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-save-report/references/widgets.md +0 -0
  80. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-top-partnerships/SKILL.md +0 -0
  81. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
  82. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-views-guarantee/SKILL.md +0 -0
  83. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/skills/tl-views-guarantee/scripts/vg.py +0 -0
  84. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/_completions.py +0 -0
  85. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/_typer_utils.py +0 -0
  86. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/auth/__init__.py +0 -0
  87. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/auth/commands.py +0 -0
  88. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/auth/login.py +0 -0
  89. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/auth/pkce.py +0 -0
  90. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/auth/token_store.py +0 -0
  91. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/client/__init__.py +0 -0
  92. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/client/http.py +0 -0
  93. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/__init__.py +0 -0
  94. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/_comments_common.py +0 -0
  95. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/balance.py +0 -0
  96. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/brands.py +0 -0
  97. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/bulk_import.py +0 -0
  98. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/changelog.py +0 -0
  99. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/channels.py +0 -0
  100. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/credits.py +0 -0
  101. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/deals.py +0 -0
  102. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/describe.py +0 -0
  103. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/doctor.py +0 -0
  104. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/matches.py +0 -0
  105. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/proposals.py +0 -0
  106. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/recommender.py +0 -0
  107. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/reports.py +0 -0
  108. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/schema.py +0 -0
  109. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/setup.py +0 -0
  110. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/snapshots.py +0 -0
  111. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/sponsorships.py +0 -0
  112. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/uploads.py +0 -0
  113. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/commands/whoami.py +0 -0
  114. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/config.py +0 -0
  115. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/filters.py +0 -0
  116. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/hints.py +0 -0
  117. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/output/__init__.py +0 -0
  118. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/output/formatter.py +0 -0
  119. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/src/tl_cli/self_update.py +0 -0
  120. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/__init__.py +0 -0
  121. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_auth.py +0 -0
  122. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_brands_winner_channels.py +0 -0
  123. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_build_report.py +0 -0
  124. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_channels_lookalike.py +0 -0
  125. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_describe.py +0 -0
  126. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_expand_entities.py +0 -0
  127. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_filters.py +0 -0
  128. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_http_auth.py +0 -0
  129. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_output.py +0 -0
  130. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_probe.py +0 -0
  131. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_reports.py +0 -0
  132. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_scripts_executable.py +0 -0
  133. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_search_channels.py +0 -0
  134. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_search_videos.py +0 -0
  135. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_select_keywords.py +0 -0
  136. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_setup.py +0 -0
  137. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests/test_sponsorships.py +0 -0
  138. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests_cli/AGENTS.md +0 -0
  139. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests_cli/conftest.py +0 -0
  140. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests_cli/test_balance.py +0 -0
  141. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests_cli/test_db_es.py +0 -0
  142. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests_cli/test_db_fb.py +0 -0
  143. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests_cli/test_db_pg.py +0 -0
  144. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests_cli/test_schema.py +0 -0
  145. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/tests_cli/test_whoami.py +0 -0
  146. {thoughtleaders_cli-0.9.0 → thoughtleaders_cli-0.9.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence
5
5
  Project-URL: Homepage, https://thoughtleaders.io
6
6
  Project-URL: Repository, https://github.com/ThoughtLeaders-io/thoughtleaders-cli
@@ -20,11 +20,9 @@ if ! tl auth status --quiet 2>/dev/null; then
20
20
  exit 0 # Don't block, just warn
21
21
  fi
22
22
 
23
- # For list commands without explicit limit, suggest adding one
24
- if [[ "$COMMAND" =~ ^tl[[:space:]]+(deals|uploads|channels|brands)[[:space:]] ]]; then
25
- if [[ ! "$COMMAND" =~ limit: ]] && [[ ! "$COMMAND" =~ --limit ]]; then
26
- echo "HINT: Consider adding a limit to control credit usage (e.g., limit:50)" >&2
27
- fi
28
- fi
23
+ # NOTE: no "add a limit" hint here. List commands are server-limited by
24
+ # default and typically cost far below the warning-worthy range, so the
25
+ # hint was pure noise for agents (it fired on every unlimited list call
26
+ # regardless of cost).
29
27
 
30
28
  exit 0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.9.0"
7
+ version = "0.9.1"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -229,12 +229,15 @@ tl <entity> comment-edit <comment-id> "msg" # Edit own comment (author or super
229
229
  ```bash
230
230
  tl sponsorships update <id> '<json>' # Edit a sponsorship (adlink)
231
231
  tl channels update <id> '<json>' # Edit a channel
232
+ tl profiles update <id> '<json>' # Edit a brand/publisher profile (superuser only)
232
233
  ```
233
234
 
234
235
  Examples:
235
236
  ```bash
236
237
  tl sponsorships update 98765 '{"publish_status": "sold"}'
237
238
  tl sponsorships update 98765 '{"publish_status": 3}'
239
+ tl profiles update 8871 '{"superuser_notes": "VIP account — always cc the AM lead"}'
240
+ tl profiles update 8871 '{"superuser_notes": null}'
238
241
  tl channels update 12345 '{"demographic_male_share": 62}'
239
242
  tl channels update 12345 '{"demographic_geo": {"US": 60, "UK": 12, "CA": 8}}'
240
243
  tl channels update 12345 '{"demographic_male_share": 55, "demographic_usa_share": 70}'
@@ -260,6 +263,13 @@ the date the channel joined the Media Selling Network, as a `YYYY-MM-DD` string.
260
263
  itself the membership flag: set it to add the channel to MSN (it then reads as `is_msn` and
261
264
  appears in MSN-filtered queries), or send `null` to remove the channel from the network.
262
265
 
266
+ **Profile notes.** `tl profiles update` (superuser only) edits a brand/publisher profile's
267
+ **`superuser_notes`** — the internal free-text notes field on the customer record
268
+ (`thoughtleaders_profile.superuser_notes`, max 2500 chars). Send a string to set, `null` to
269
+ clear. This is currently the only editable profile field; anything else returns a 400 listing
270
+ the editable surface. Use it for account-handling notes meant for the internal team — it is
271
+ never shown to the customer.
272
+
263
273
  ### Creating and vetting sponsorships
264
274
 
265
275
  This is the end-to-end workflow for proposing a sponsorship, then moving it through the funnel as the two sides respond. Three create commands plus `tl sponsorships update` cover every state transition the CLI exposes.
@@ -69,6 +69,7 @@ Maps business terms to database concepts.
69
69
  | `owner_advertiser_id` | `profile` | **Account owner.** Who owns the brand relationship overall. Often same person as owner_sales on adlinks, but not always. |
70
70
  | `owner_publisher_id` | `profile` | Channel relationship owner on the profile level |
71
71
  | `owner_sales_id` | `profile` | Sales owner at profile level |
72
+ | `superuser_notes` | `profile` | Internal free-text account-handling notes on the customer record (max 2500 chars). Team-facing only — never shown to the customer. Superusers can edit it through the CLI (see SKILL.md → "Updating records"). |
72
73
 
73
74
  **Key insight:** Ownership exists on both `profile` (account-level) and `adlink` (deal-level). For revenue attribution, always use `adlink.owner_sales_id`.
74
75
 
@@ -33,10 +33,12 @@ Drop to `tl db fb` only when you need a shape `tl snapshots` doesn't produce (cu
33
33
  - **SELECT only.** No DDL/DML/transactions/SET/locks.
34
34
  - **Single table.** No JOIN, CTE (`WITH`), subquery, set operation, or `LATERAL`.
35
35
  - **Only known tables:** `article_metrics`, `channel_metrics`. Other names return `UNKNOWN_TABLE`.
36
+ - **Constant-only probes are allowed:** `SELECT 1` (no FROM, no column references) works as a connectivity check. Anything naming a column or `*` still needs a FROM.
36
37
  - **WHERE/HAVING may only reference indexed columns** (`channel_id`/`id` for `article_metrics`; `id` for `channel_metrics`). Filtering by `age`, `publication_date`, `view_count`, `duration`, `scrape_date`, etc. in WHERE returns `NON_INDEXED_FILTER:<col>`. Apply those constraints client-side after fetching.
37
38
  - **Leading index column must be equality-or-IN-filtered with literals** (`channel_id = 1` or `channel_id IN (1,2,3)`). Without it: `MISSING_INDEXED_FILTER`.
38
39
  - **Trivial-aggregation exception:** a SELECT whose projected expressions are all aggregates and which has no GROUP BY / HAVING may omit WHERE entirely. Use only for tiny sanity checks.
39
40
  - **No mandatory LIMIT/OFFSET** — but Firebolt will time out on bad plans, so keep the leading-index filter selective.
41
+ - **Errors carry the real diagnostic.** A query that Firebolt itself rejects (unknown column, syntax error) comes back as a 400 with Firebolt's own error description, plus a rename hint where one is known. Common trap: `channel_metrics` has `subscribers`, not the older `reach`.
40
42
 
41
43
  ## Tables
42
44
 
@@ -275,6 +275,7 @@ JOIN thoughtleaders_profile_brands pb ON p.id = pb.profile_id
275
275
  JOIN thoughtleaders_brand b ON pb.brand_id = b.id
276
276
  -- NEVER: JOIN brand b ON b.id = a.advertiser_profile_id (different ID spaces, returns wrong data)
277
277
  ```
278
+ This 3-table join is the one brand path that works in every session — use it in anything reusable.
278
279
 
279
280
  **Adlink → Organization:**
280
281
  ```sql
@@ -41,7 +41,7 @@ This skill is self-contained. Every reference it needs is in [`references/`](ref
41
41
  | **3** | CHANNELS | one YouTube channel (or podcast) | intelligence |
42
42
  | **8** | SPONSORSHIPS / Deals | one sponsorship record (AdLink — brand × channel × dates × status × price) | sponsorship |
43
43
 
44
- Types 1 / 2 / 3 share the intelligence FilterSet and widget schemas (different rows, same predicate fields). Type 8 has its own schemas (disjoint fields, different aggregators, different data plane — Postgres against `v_adspot_brand_profiles` rather than Elasticsearch).
44
+ Types 1 / 2 / 3 share the intelligence FilterSet and widget schemas (different rows, same predicate fields). Type 8 has its own schemas (disjoint fields, different aggregators, different data plane — a denormalized sponsorship dataset in Postgres rather than Elasticsearch).
45
45
 
46
46
  ## When to invoke
47
47
 
@@ -10,7 +10,7 @@
10
10
  "django_model": "dashboard.models.FilterSet",
11
11
  "django_enums_module": "dashboard.enums",
12
12
  "applies_to_report_types": [8],
13
- "report_type_meaning": "CAMPAIGN_MANAGEMENT — sponsorship/AdLink-level reporting. Filters are evaluated against the denormalized view v_adspot_brand_profiles, which already joins adlink, brand, channel, and organization. Direct joins from thoughtleaders_adlink to thoughtleaders_brand or thoughtleaders_channel are NOT possible — those FKs do not exist on the adlink table. See SKILL.md Step 2.V1 (Sponsorship reports section) for the canonical query shape.",
13
+ "report_type_meaning": "CAMPAIGN_MANAGEMENT — sponsorship/AdLink-level reporting. Filters are evaluated against a denormalized sponsorship dataset that already joins adlink, brand, channel, and organization. Direct joins from thoughtleaders_adlink to thoughtleaders_brand or thoughtleaders_channel are NOT possible — those FKs do not exist on the adlink table. See SKILL.md Step 2.V1 (Sponsorship reports section) for the canonical query shape.",
14
14
  "note": "Same Django model as the intelligence schema, but only a subset of fields is meaningful for sponsorship reporting. Fields not listed here exist on the model but are inert for type 8."
15
15
  },
16
16
 
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.9.0"
3
+ __version__ = "0.9.1"
@@ -68,6 +68,17 @@ def handle_api_error(error: ApiError) -> None:
68
68
  _print_debug(error)
69
69
  sys.exit(3)
70
70
  else:
71
- err.print(f"[red]Error ({error.status_code}):[/red] {error.detail}")
71
+ detail = error.detail or ""
72
+ hint = (error.raw or {}).get("hint") if isinstance(error.raw, dict) else None
73
+ if isinstance(hint, str) and hint:
74
+ # The server sends the remediation both concatenated into
75
+ # `detail` (for older clients) and as a separate `hint` key —
76
+ # render it on its own line so it can't get lost in the error.
77
+ if detail.endswith(hint):
78
+ detail = detail[: -len(hint)].rstrip()
79
+ err.print(f"[red]Error ({error.status_code}):[/red] {detail}")
80
+ err.print(f"[bold yellow]Hint:[/bold yellow] [yellow]{hint}[/yellow]")
81
+ else:
82
+ err.print(f"[red]Error ({error.status_code}):[/red] {detail}")
72
83
  _print_debug(error)
73
84
  sys.exit(1)
@@ -5,12 +5,12 @@ import sys
5
5
 
6
6
  import typer
7
7
  from rich.console import Console
8
- from tl_cli._typer_utils import AlphaSortedTyperGroup
9
8
 
9
+ from tl_cli._typer_utils import AlphaSortedTyperGroup
10
10
  from tl_cli.client.errors import ApiError, handle_api_error
11
11
  from tl_cli.client.http import get_client
12
12
  from tl_cli.output.formatter import detect_format, output, output_pricing_estimate
13
- from tl_cli.query_history import query_hash, record_and_check
13
+ from tl_cli.query_history import note_charge, query_hash, record_and_check
14
14
 
15
15
  app = typer.Typer(cls=AlphaSortedTyperGroup, help="Raw read-only queries against PostgreSQL, Firebolt, or Elasticsearch (full-access only)")
16
16
 
@@ -18,7 +18,10 @@ _err = Console(stderr=True)
18
18
 
19
19
  _NO_REPEAT_WARNING_OPTION = typer.Option(
20
20
  False, "--no-repeat-warning",
21
- help="Suppress the warning shown when the same query is re-run repeatedly (also: TL_NO_REPEAT_WARNING=1).",
21
+ help=(
22
+ "Suppress the warning shown when the same query is re-run repeatedly after "
23
+ "burning 1000+ credits (also: TL_NO_REPEAT_WARNING=1)."
24
+ ),
22
25
  )
23
26
 
24
27
 
@@ -30,28 +33,37 @@ def _read_query(query: str | None) -> str:
30
33
  return sys.stdin.read()
31
34
 
32
35
 
33
- def _warn_if_repeat(engine: str, query: str, pricing: bool, suppressed: bool) -> None:
34
- """Nudge (on stderr) when this exact query keeps being re-run.
36
+ def _warn_if_repeat(digest: str, suppressed: bool) -> None:
37
+ """Nudge (on stderr) when this exact query keeps being re-run AND the
38
+ repeats have already burned a material amount of credits.
35
39
 
36
- Detection is local and best-effort; the query always executes.
40
+ Detection is local and best-effort; the query always executes. Cheap
41
+ repeats stay silent — the warning only fires once the window's runs
42
+ have been billed SPEND_THRESHOLD_CREDITS in total.
37
43
  """
38
44
  if suppressed:
39
45
  return
40
- count = record_and_check(query_hash(engine, query, pricing))
41
- if count is None:
46
+ due = record_and_check(digest)
47
+ if due is None:
42
48
  return
49
+ count, spent = due
43
50
  _err.print(
44
51
  f"[yellow]Repeat query:[/yellow] this exact query has now run {count} times "
45
- "in the last 5 minutes. Each run is billed and the results are unlikely to "
46
- "differ — reuse the earlier output (e.g. save it with `--json > file.json`). "
47
- "Pass --no-repeat-warning if the repeats are deliberate."
52
+ f"in the last 5 minutes ({spent:g} credits so far). Each run is billed and "
53
+ "the results are unlikely to differ — reuse the earlier output (e.g. save it "
54
+ "with `--json > file.json`). Pass --no-repeat-warning if the repeats are "
55
+ "deliberate."
48
56
  )
49
57
 
50
58
 
51
- def _run(path: str, body: dict, fmt: str, title: str, pricing: bool = False) -> None:
59
+ def _run(path: str, body: dict, fmt: str, title: str, pricing: bool = False,
60
+ digest: str | None = None) -> None:
52
61
  client = get_client()
53
62
  try:
54
63
  data = client.post(path, json_body=body)
64
+ if digest:
65
+ usage = data.get("usage") or {}
66
+ note_charge(digest, usage.get("credits_charged"))
55
67
  if pricing:
56
68
  output_pricing_estimate(data, fmt)
57
69
  return
@@ -84,9 +96,19 @@ def pg_cmd(
84
96
  ) -> None:
85
97
  """Run a raw PostgreSQL SELECT query.
86
98
 
87
- Column names follow the current schema (e.g. subscribers, projected_views,
88
- scheduled_date — not the older reach / impression / send_date). Run
89
- `tl schema pg <table>` to see the exact columns your role can query.
99
+ Common traps check before writing SQL:
100
+
101
+ - Column names follow the current schema: subscribers, projected_views,
102
+ scheduled_date, is_tpp, advertiser_profile_id, views_guarantee — NOT the
103
+ older reach / impression / send_date / is_tl_channel / creator_profile_id
104
+ / impressions_guarantee.
105
+ - The sponsorship table (thoughtleaders_adlink) hides its relations behind
106
+ joins: no channel_id/channel_name (join thoughtleaders_adspot via
107
+ ad_spot_id, then thoughtleaders_channel) and no brand_id (join
108
+ thoughtleaders_profile via advertiser_profile_id, then
109
+ thoughtleaders_profile_brands).
110
+ - Run `tl schema pg <table>` first — it lists the exact columns your role
111
+ can query plus each table's common traps.
90
112
 
91
113
  Examples:
92
114
  tl db pg "SELECT channel_name, subscribers, projected_views FROM thoughtleaders_channel ORDER BY subscribers DESC LIMIT 10"
@@ -96,11 +118,12 @@ def pg_cmd(
96
118
  """
97
119
  fmt = detect_format(json_output, csv_output, md_output, toon_output)
98
120
  sql = _read_query(query)
99
- _warn_if_repeat("pg", sql, pricing, no_repeat_warning)
121
+ digest = query_hash("pg", sql, pricing)
122
+ _warn_if_repeat(digest, no_repeat_warning)
100
123
  body: dict = {"query": sql}
101
124
  if pricing:
102
125
  body["pricing"] = True
103
- _run("/raw/pg", body, fmt, "Postgres results", pricing=pricing)
126
+ _run("/raw/pg", body, fmt, "Postgres results", pricing=pricing, digest=digest)
104
127
 
105
128
 
106
129
  @app.command("fb")
@@ -118,19 +141,28 @@ def fb_cmd(
118
141
  ) -> None:
119
142
  """Run a raw Firebolt SELECT query.
120
143
 
121
- The query must filter the leading index column of the table (channel_id
122
- for article_metrics, id for channel_metrics) see the Firebolt schema.
144
+ Two tables: article_metrics (per-video daily metrics) and channel_metrics
145
+ (per-channel history). Single-table SELECTs only (no JOINs), and WHERE
146
+ must equality- or IN-filter the leading index column — channel_id for
147
+ article_metrics, id (the channel id) for channel_metrics. Other columns
148
+ can't be filtered server-side: fetch by the indexed key, then narrow
149
+ client-side. Column names follow the current schema (subscribers, not
150
+ the older reach) — run `tl schema fb` to see them.
123
151
 
124
152
  Examples:
153
+ tl db fb "SELECT 1" # connectivity check — the only no-FROM query allowed
125
154
  tl db fb "SELECT scrape_date, view_count FROM article_metrics WHERE channel_id = 5607 AND id = 'EjeGzoQI3gQ'"
155
+ tl db fb "SELECT scrape_date, subscribers, total_views FROM channel_metrics WHERE id = 736006 ORDER BY scrape_date"
156
+ tl db fb "SELECT publication_date, view_count FROM article_metrics WHERE channel_id IN (5607, 5608)"
126
157
  """
127
158
  fmt = detect_format(json_output, csv_output, md_output, toon_output)
128
159
  sql = _read_query(query)
129
- _warn_if_repeat("fb", sql, pricing, no_repeat_warning)
160
+ digest = query_hash("fb", sql, pricing)
161
+ _warn_if_repeat(digest, no_repeat_warning)
130
162
  body: dict = {"query": sql}
131
163
  if pricing:
132
164
  body["pricing"] = True
133
- _run("/raw/fb", body, fmt, "Firebolt results", pricing=pricing)
165
+ _run("/raw/fb", body, fmt, "Firebolt results", pricing=pricing, digest=digest)
134
166
 
135
167
 
136
168
  @app.command("es")
@@ -166,10 +198,11 @@ def es_cmd(
166
198
  except json.JSONDecodeError as exc:
167
199
  raise typer.BadParameter(f"Query is not valid JSON: {exc}") from exc
168
200
 
169
- _warn_if_repeat("es", raw, pricing, no_repeat_warning)
201
+ digest = query_hash("es", raw, pricing)
202
+ _warn_if_repeat(digest, no_repeat_warning)
170
203
  body: dict = {"query": body_query}
171
204
  if pricing:
172
205
  body["pricing"] = True
173
206
  if highlight:
174
207
  body["include_highlight"] = True
175
- _run("/raw/es", body, fmt, "Elasticsearch results", pricing=pricing)
208
+ _run("/raw/es", body, fmt, "Elasticsearch results", pricing=pricing, digest=digest)
@@ -0,0 +1,59 @@
1
+ """tl profiles — Brand/publisher profile operations."""
2
+
3
+ import json as _json
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from tl_cli._typer_utils import AlphaSortedTyperGroup
9
+ from tl_cli.client.errors import ApiError, handle_api_error
10
+ from tl_cli.client.http import get_client
11
+ from tl_cli.output.formatter import detect_format, output_single
12
+
13
+ app = typer.Typer(cls=AlphaSortedTyperGroup, help="Brand/publisher profiles (update; full-access only)")
14
+
15
+
16
+ @app.callback()
17
+ def _profiles() -> None:
18
+ """Brand/publisher profiles.
19
+
20
+ The callback keeps `update` an explicit subcommand — without it Typer
21
+ collapses a single-command group into the group itself.
22
+ """
23
+
24
+
25
+ @app.command("update")
26
+ def update_cmd(
27
+ profile_id: int = typer.Argument(..., help="Profile ID (numeric)"),
28
+ fields: str = typer.Argument(..., help="JSON object of fields to update"),
29
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
30
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
31
+ ) -> None:
32
+ """Update a profile. Full-access only.
33
+
34
+ Editable fields: superuser_notes (free-text, max 2500 chars; send null
35
+ to clear). Unknown fields are rejected with a 400 listing the
36
+ offending key.
37
+
38
+ Examples:
39
+ tl profiles update 8871 '{"superuser_notes": "VIP account — always cc the AM lead"}'
40
+ tl profiles update 8871 '{"superuser_notes": null}'
41
+ """
42
+ fmt = detect_format(json_output, False, False, toon_output)
43
+ try:
44
+ body = _json.loads(fields)
45
+ except _json.JSONDecodeError as exc:
46
+ Console(stderr=True).print(f"[red]Error:[/red] fields argument must be a JSON object: {exc}")
47
+ raise typer.Exit(1)
48
+ if not isinstance(body, dict):
49
+ Console(stderr=True).print("[red]Error:[/red] fields argument must be a JSON object.")
50
+ raise typer.Exit(1)
51
+
52
+ client = get_client()
53
+ try:
54
+ data = client.post(f"/profiles/{profile_id}/edit", json_body=body)
55
+ output_single(data, fmt)
56
+ except ApiError as e:
57
+ handle_api_error(e)
58
+ finally:
59
+ client.close()
@@ -22,6 +22,7 @@ from tl_cli.commands.channels import app as channels_app
22
22
  from tl_cli.commands.db import app as db_app
23
23
  from tl_cli.commands.deals import app as deals_app
24
24
  from tl_cli.commands.matches import app as matches_app
25
+ from tl_cli.commands.profiles import app as profiles_app
25
26
  from tl_cli.commands.proposals import app as proposals_app
26
27
  from tl_cli.commands.recommender import app as recommender_app
27
28
  from tl_cli.commands.sponsorships import app as sponsorships_app
@@ -93,6 +94,7 @@ app.add_typer(deals_app, name="deals")
93
94
  app.add_typer(uploads_app, name="uploads")
94
95
  app.add_typer(channels_app, name="channels")
95
96
  app.add_typer(brands_app, name="brands")
97
+ app.add_typer(profiles_app, name="profiles")
96
98
  app.add_typer(recommender_app, name="recommender")
97
99
  app.add_typer(snapshots_app, name="snapshots")
98
100
  app.add_typer(reports_app, name="reports")
@@ -25,6 +25,12 @@ WINDOW_S = 300
25
25
  REPEAT_THRESHOLD = 3
26
26
  WARN_INTERVAL_S = 60
27
27
 
28
+ # Materiality floor: repeats only warn once the credits they have already
29
+ # burned inside the window reach this amount. Cheap queries re-run in a loop
30
+ # are noise to warn about — the warning exists to stop material wasted spend,
31
+ # not to nag an agent that is legitimately polling an inexpensive count.
32
+ SPEND_THRESHOLD_CREDITS = 1000
33
+
28
34
  ENV_SUPPRESS = "TL_NO_REPEAT_WARNING"
29
35
 
30
36
 
@@ -40,11 +46,16 @@ def query_hash(engine: str, query: str, pricing: bool = False) -> str:
40
46
  return hashlib.sha256(basis.encode("utf-8")).hexdigest()
41
47
 
42
48
 
43
- def record_and_check(digest: str) -> int | None:
44
- """Record one run of ``digest``; return the run count when a warning is due.
49
+ def record_and_check(digest: str) -> tuple[int, float] | None:
50
+ """Record one run of ``digest``; return ``(run_count, credits_spent)``
51
+ when a warning is due.
45
52
 
46
- Returns None below the threshold, while a warning from the last
47
- WARN_INTERVAL_S is still fresh, or when TL_NO_REPEAT_WARNING is set.
53
+ A warning is due when the run count reaches REPEAT_THRESHOLD inside the
54
+ window AND the previous runs have already been billed at least
55
+ SPEND_THRESHOLD_CREDITS in total (charges are attached post-flight via
56
+ ``note_charge``). Returns None below either threshold, while a warning
57
+ from the last WARN_INTERVAL_S is still fresh, or when
58
+ TL_NO_REPEAT_WARNING is set.
48
59
  """
49
60
  if os.environ.get(ENV_SUPPRESS):
50
61
  return None
@@ -53,20 +64,69 @@ def record_and_check(digest: str) -> int | None:
53
64
  entry = state.get(digest)
54
65
  entry = entry if isinstance(entry, dict) else {}
55
66
  runs = _recent_runs(entry, now)
56
- runs.append(now)
67
+ spent = sum(charged for _, charged in runs)
68
+ runs.append([now, 0.0])
57
69
  warned_at = entry.get("warned_at")
58
70
  warning_fresh = isinstance(warned_at, int | float) and now - warned_at < WARN_INTERVAL_S
59
- due = len(runs) >= REPEAT_THRESHOLD and not warning_fresh
71
+ due = (
72
+ len(runs) >= REPEAT_THRESHOLD
73
+ and spent >= SPEND_THRESHOLD_CREDITS
74
+ and not warning_fresh
75
+ )
60
76
  state[digest] = {"runs": runs, "warned_at": now if due else warned_at}
61
77
  _save(state, now)
62
- return len(runs) if due else None
78
+ return (len(runs), spent) if due else None
79
+
63
80
 
81
+ def note_charge(digest: str, charged: object) -> None:
82
+ """Attach the billed charge to ``digest``'s most recent run.
64
83
 
65
- def _recent_runs(entry: dict, now: float) -> list[float]:
84
+ Called post-flight with ``usage.credits_charged`` from the response
85
+ envelope — the pre-flight ``record_and_check`` can't know what the run
86
+ will cost. Missing/zero/bogus charges are ignored (the run then counts
87
+ as free, which only makes the warning more conservative).
88
+ """
89
+ try:
90
+ amount = float(charged) # type: ignore[arg-type]
91
+ except (TypeError, ValueError):
92
+ return
93
+ if amount <= 0:
94
+ return
95
+ now = time.time()
96
+ state = _load()
97
+ entry = state.get(digest)
98
+ if not isinstance(entry, dict):
99
+ return
100
+ runs = _recent_runs(entry, now)
101
+ if not runs:
102
+ return
103
+ runs[-1][1] = amount
104
+ state[digest] = {"runs": runs, "warned_at": entry.get("warned_at")}
105
+ _save(state, now)
106
+
107
+
108
+ def _recent_runs(entry: dict, now: float) -> list[list[float]]:
109
+ """Window-fresh ``[timestamp, credits_charged]`` pairs from ``entry``.
110
+
111
+ Pre-charge-tracking state files stored bare timestamps; those are read
112
+ as charge-0 runs rather than resetting the user's history.
113
+ """
66
114
  raw = entry.get("runs")
67
115
  if not isinstance(raw, list):
68
116
  return []
69
- return [t for t in raw if isinstance(t, int | float) and now - t < WINDOW_S]
117
+ runs: list[list[float]] = []
118
+ for item in raw:
119
+ if isinstance(item, int | float):
120
+ item = [item, 0.0]
121
+ if (
122
+ isinstance(item, list)
123
+ and len(item) == 2
124
+ and isinstance(item[0], int | float)
125
+ and isinstance(item[1], int | float)
126
+ and now - item[0] < WINDOW_S
127
+ ):
128
+ runs.append([float(item[0]), float(item[1])])
129
+ return runs
70
130
 
71
131
 
72
132
  def _load() -> dict: