thoughtleaders-cli 0.7.6__py3-none-any.whl → 0.7.8__py3-none-any.whl
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.
- {thoughtleaders_cli-0.7.6.dist-info → thoughtleaders_cli-0.7.8.dist-info}/METADATA +8 -4
- {thoughtleaders_cli-0.7.6.dist-info → thoughtleaders_cli-0.7.8.dist-info}/RECORD +13 -13
- tl_cli/__init__.py +1 -1
- tl_cli/_plugin/.claude-plugin/plugin.json +1 -1
- tl_cli/_plugin/skills/tl/SKILL.md +3 -2
- tl_cli/auth/commands.py +1 -3
- tl_cli/auth/login.py +1 -1
- tl_cli/commands/describe.py +20 -19
- tl_cli/commands/recommender.py +48 -0
- tl_cli/output/formatter.py +13 -7
- {thoughtleaders_cli-0.7.6.dist-info → thoughtleaders_cli-0.7.8.dist-info}/WHEEL +0 -0
- {thoughtleaders_cli-0.7.6.dist-info → thoughtleaders_cli-0.7.8.dist-info}/entry_points.txt +0 -0
- {thoughtleaders_cli-0.7.6.dist-info → thoughtleaders_cli-0.7.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.8
|
|
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
|
|
@@ -163,6 +163,7 @@ tl recommender tags cooking # Search tag names by substring
|
|
|
163
163
|
tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
|
|
164
164
|
tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
|
|
165
165
|
tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
|
|
166
|
+
tl recommender channels-with-tag "Cooking" # ALL channel IDs loaded on a tag (--min defaults to 0.00001; paged; 1 credit/result)
|
|
166
167
|
tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
|
|
167
168
|
tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal profile
|
|
168
169
|
tl recommender channels-for-profile 842 # Channels closest to a specific brand profile
|
|
@@ -210,9 +211,9 @@ tl describe show sponsorships --filters # Available filters for sponsorships
|
|
|
210
211
|
tl balance # Your credit balance
|
|
211
212
|
```
|
|
212
213
|
|
|
213
|
-
`tl db pg` is priced **per-
|
|
214
|
+
`tl db pg` is priced **per-row**: the per-row rate is the **sum of the rates of the tables the query touches** (default 1.0/row; some tables are cheaper or dearer), plus a flat per-row charge for every expensive column read (demographics, channel outreach emails), all times the rows returned. Aggregate queries (`count`/`GROUP BY`) add a surcharge proportional to the rows they aggregate. Run `tl describe show db --json` to see the live `pg_pricing` map, and check `usage.credit_rate` in the response envelope after a query to see what your query was actually charged.
|
|
214
215
|
|
|
215
|
-
To preview a query's cost **before** running it, add `--pricing`: `tl db pg "SELECT … LIMIT 100" --pricing` runs only the planner's `EXPLAIN`, prints the cost breakdown and an upper-bound estimate (at the query's `LIMIT`), and costs a flat **1 credit** — the query itself never executes. Works with `--json` too. `--pricing` is also available on `tl db fb` and `tl db es`; those backends
|
|
216
|
+
To preview a query's cost **before** running it, add `--pricing`: `tl db pg "SELECT … LIMIT 100" --pricing` runs only the planner's `EXPLAIN`, prints the cost breakdown and an upper-bound estimate (at the query's `LIMIT`), and costs a flat **1 credit** — the query itself never executes. Works with `--json` too. `--pricing` is also available on `tl db fb` and `tl db es`; those backends have no per-table or per-column charges, so the estimate is the flat per-row rate at the query's row ceiling (`LIMIT` for Firebolt, `size` — or the aggregation doc cap — for Elasticsearch).
|
|
216
217
|
|
|
217
218
|
# Terminology
|
|
218
219
|
|
|
@@ -277,8 +278,11 @@ Each agent discovers the skill automatically and uses it when you ask about spon
|
|
|
277
278
|
The plugin ships several focused skills (installed by all the `tl setup *` commands):
|
|
278
279
|
|
|
279
280
|
- **`tl`** — the data-analyst skill. Defaults to raw database queries via `tl db pg|fb|es` for anything non-trivial; uses the structured `tl <resource> show` / `find` / `similar` commands for single-record lookups and similarity / ID-resolution special cases. Comes with full schema references for Postgres, Elasticsearch, and Firebolt under `references/`.
|
|
280
|
-
- **`tl-
|
|
281
|
+
- **`tl-keyword-research`** — broadens and ranks content-search keywords by Elasticsearch document count before a `tl db es` content search, so finding videos or channels by topic isn't bottlenecked on hand-guessed terms.
|
|
282
|
+
- **`tl-save-report`** — persists the result set from an in-chat exploration session as a saved TL report ("save this as a report", "turn this into a campaign").
|
|
283
|
+
- **`tl-report-builder`** — builds a brand-new TL report config from scratch (channels / brands / sponsorships / videos) through a guided multi-phase flow. Manual-invocation-only: reach it via `/tl-report-builder` or by naming it explicitly — natural-language report requests route to `tl`, `tl-save-report`, or `tl-import` instead.
|
|
281
284
|
- **`tl-import`** / **`bulk-import`** — superuser-only; bulk-add or exclude lists of channels, brands, videos, or sponsorships against a report.
|
|
285
|
+
- **`tl-channel-authenticity`** — vets a YouTube channel for non-organic views and bot/spam comments before booking (or after delivering) a sponsorship.
|
|
282
286
|
- **`tl-views-guarantee`** — sizes a multi-video sponsorship buy for a channel, returning the video bundle size, views guarantee, and likelihood to hit.
|
|
283
287
|
- **`tl-top-partnerships`** — brand-user performance report. Ranks a brand's sold sponsorships by live eCPM vs the sold-date projection, aggregates per channel, and delivers a two-tab Google Sheet ("By Deal" / "By Channel") via `gws`. Uses only public CLI commands (`tl whoami`, `tl sponsorships list`).
|
|
284
288
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
tl_cli/__init__.py,sha256=
|
|
1
|
+
tl_cli/__init__.py,sha256=P8v-VIzxtaC--QFovDaGF5D5jVNjkjwOqcucG5_5_I0,112
|
|
2
2
|
tl_cli/_completions.py,sha256=kOyEUqC26vbYvyXWi513WX8fF73qQLR5WWuRSe_wqyk,164
|
|
3
3
|
tl_cli/_typer_utils.py,sha256=ZiZsCVmEznPvBw-dYbr3tu3zWZ0iN6kjoQmK3gMqD28,860
|
|
4
4
|
tl_cli/config.py,sha256=UV_OYTXuQnAIqbi_oVCXx0hhIdZWR678RRapVv51UwQ,1859
|
|
@@ -7,8 +7,8 @@ tl_cli/hints.py,sha256=cT8kuDtkAZqwXkc2RV0Yg_abofK-g9UiXwTTBunX78U,1557
|
|
|
7
7
|
tl_cli/main.py,sha256=A_8b2SQjBKATxrjO7AGC5Ab1QWlP35gGo4TWzYZtlOM,5806
|
|
8
8
|
tl_cli/self_update.py,sha256=akXOWYgBX2otyaVlx9CDl04gG2s_hYigE2Vkpubt0SA,18302
|
|
9
9
|
tl_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
tl_cli/auth/commands.py,sha256=
|
|
11
|
-
tl_cli/auth/login.py,sha256=
|
|
10
|
+
tl_cli/auth/commands.py,sha256=CkCaKFb-xUwhCeIL92EC4-odiaSpLI1bmgg5tDCrclM,7387
|
|
11
|
+
tl_cli/auth/login.py,sha256=AxdQ8LOZd1uZhFXPyaiGB_Hk0RiVuX7t37gkjgoEOCM,11568
|
|
12
12
|
tl_cli/auth/pkce.py,sha256=4Q6Ip-TeZFNG9c3swXNi4gH7mdMkltKa62gZZNybt8U,658
|
|
13
13
|
tl_cli/auth/token_store.py,sha256=TcZnUol4-8r0jMEJhOPmABCX12_5RkAln2xfWPNdmHk,3275
|
|
14
14
|
tl_cli/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -24,11 +24,11 @@ tl_cli/commands/channels.py,sha256=ALw2fgJL3w0dpYp3A41OECL0odenhSV16vFA7la7aQI,1
|
|
|
24
24
|
tl_cli/commands/credits.py,sha256=2xCht2e420LmaFBKNdKoMz8GlTh31qSWSlJAnVzoZic,7308
|
|
25
25
|
tl_cli/commands/db.py,sha256=rdIQrxT7sdrPEnBbByNHvPr2X6iIg-wb19X9bWYwDRc,5053
|
|
26
26
|
tl_cli/commands/deals.py,sha256=ZK9yneInsC6DXoCPS65oyLoVR0eRW1xdRlEN7oRp1pc,2174
|
|
27
|
-
tl_cli/commands/describe.py,sha256=
|
|
27
|
+
tl_cli/commands/describe.py,sha256=3lURv4NllM5qPeMEBbejrIxiMzsyTwpJIz211-wuGCU,12362
|
|
28
28
|
tl_cli/commands/doctor.py,sha256=KUKglwhMc7B26XXy_3M0LkHu7wqfFO5T0YPHO1SH1VY,9024
|
|
29
29
|
tl_cli/commands/matches.py,sha256=K5o6B8FLECp7825dU4W3X8n-wuXvGJz57xpQPXeXQ-0,2886
|
|
30
30
|
tl_cli/commands/proposals.py,sha256=khsjorluIfgrJ22DiwzIAFcYD4JbirjkOBz1KuQ0Sdk,2918
|
|
31
|
-
tl_cli/commands/recommender.py,sha256=
|
|
31
|
+
tl_cli/commands/recommender.py,sha256=BZfviKAPhpZah0zaSyH5RDa23fG8QAkUnG5FDhOdi4I,21324
|
|
32
32
|
tl_cli/commands/reports.py,sha256=WcZWtBVRX49z-1Fw4T5iq_xZqks3QmIXlWVg5iVGs58,26532
|
|
33
33
|
tl_cli/commands/schema.py,sha256=GCBEE4fDatQhVasLKKr7bkGhELRZ0scYm_hUCbDYmuA,5985
|
|
34
34
|
tl_cli/commands/setup.py,sha256=QST6DCSxJHLFNX1UUJHwZ3hTDTnySATaV2Tc2JsdYYY,21920
|
|
@@ -37,16 +37,16 @@ tl_cli/commands/sponsorships.py,sha256=MWjyaReMMhmVKAbrCBCVw_J6dzkML_TIo_2kyPOYQ
|
|
|
37
37
|
tl_cli/commands/uploads.py,sha256=Tf9tqAEm9FGe3A7sr_EDX9OzdNInCmrWNr10wWGuMUo,1526
|
|
38
38
|
tl_cli/commands/whoami.py,sha256=aUXwBRwh1vAGrvz8CKGfHYtEOKJCIDfwrGesKAwYZMk,7866
|
|
39
39
|
tl_cli/output/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
|
-
tl_cli/output/formatter.py,sha256=
|
|
40
|
+
tl_cli/output/formatter.py,sha256=zWwcg4yovMXLaduxu8skpDjPVLTaGZAmwtYwjpZDg1w,22766
|
|
41
41
|
tl_cli/_plugin/.claude-plugin/marketplace.json,sha256=l56PMmyjfGXNGlV30wRyOAe74B6gJNCVNCxgsBbSNxc,446
|
|
42
|
-
tl_cli/_plugin/.claude-plugin/plugin.json,sha256=
|
|
42
|
+
tl_cli/_plugin/.claude-plugin/plugin.json,sha256=kSWDf14vaAKSZIa62p4eXV9VHtaO7E7gxoPf_RokUds,466
|
|
43
43
|
tl_cli/_plugin/agents/tl-analyst.md,sha256=6J3X3NANkWg6OOUCvNirkN4ulIk80KSumPncDUBt75E,6761
|
|
44
44
|
tl_cli/_plugin/agents/youtube-comment-classifier.md,sha256=S5lr_htA98FIX0su8FJ2ntiHfbdK8OB2NQKC4lTnQcw,2178
|
|
45
45
|
tl_cli/_plugin/hooks/hooks.json,sha256=FSWibw1xAjA-suFV3fR8btIb2kQ82LQ08otTr-NpmFw,835
|
|
46
46
|
tl_cli/_plugin/hooks/scripts/load-tl-skill.mjs,sha256=EBsyZ-caei-CBJsRtqzJXJs_20O3H22MuVmDpu96umo,805
|
|
47
47
|
tl_cli/_plugin/hooks/scripts/post-usage.sh,sha256=WVvZLkZik6lbeZ20Kh-wgm4JkRFHFN0Uwl4C8S3Y0sY,759
|
|
48
48
|
tl_cli/_plugin/hooks/scripts/pre-check.sh,sha256=E9KeuXy6yeHEBOnOFW4hDW-Et-Dbp1Oh--3WXKfOX78,898
|
|
49
|
-
tl_cli/_plugin/skills/tl/SKILL.md,sha256=
|
|
49
|
+
tl_cli/_plugin/skills/tl/SKILL.md,sha256=B9UMrGJduF3nOxESSicKYM5K5G91NcCbmYQjQh6faVk,61661
|
|
50
50
|
tl_cli/_plugin/skills/tl/references/business-glossary.md,sha256=FCS-qBOGpdJCmHdglRGRjAuTQAtzpxJNpMkEWThuvlI,17779
|
|
51
51
|
tl_cli/_plugin/skills/tl/references/elasticsearch-schema.md,sha256=OpHvixZ8UcZYJd8GdwgumryFyKwPAxj3AvPkl1QreMY,9316
|
|
52
52
|
tl_cli/_plugin/skills/tl/references/firebolt-schema.md,sha256=KagpSWWEWIRfsAWz271PvAqVbSPvWLoogWhCA_XFSZw,10642
|
|
@@ -110,8 +110,8 @@ tl_cli/_plugin/skills/tl-top-partnerships/SKILL.md,sha256=hvH05hIaGlc0RfTE0GLBtD
|
|
|
110
110
|
tl_cli/_plugin/skills/tl-top-partnerships/scripts/top_partnerships.py,sha256=_13W6-HuD_jtl7AWQQcZQ0SQO9qODMymlcL-1s4-VwU,13248
|
|
111
111
|
tl_cli/_plugin/skills/tl-views-guarantee/SKILL.md,sha256=IH7q1WJDWri9TWJMiga1FMGJO_GKSbWwaDS6CVNZ9c0,9270
|
|
112
112
|
tl_cli/_plugin/skills/tl-views-guarantee/scripts/vg.py,sha256=Qp5poinHEqh9374anq0bLtlxj2YL6ipBicaT960-Cws,15825
|
|
113
|
-
thoughtleaders_cli-0.7.
|
|
114
|
-
thoughtleaders_cli-0.7.
|
|
115
|
-
thoughtleaders_cli-0.7.
|
|
116
|
-
thoughtleaders_cli-0.7.
|
|
117
|
-
thoughtleaders_cli-0.7.
|
|
113
|
+
thoughtleaders_cli-0.7.8.dist-info/METADATA,sha256=uVUM2ngztGKX4s6AY0DGVJzxnM2eEEi3ogta_oK9puQ,19387
|
|
114
|
+
thoughtleaders_cli-0.7.8.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
115
|
+
thoughtleaders_cli-0.7.8.dist-info/entry_points.txt,sha256=umZp-1BkGkHDG0bNZXpTXrjwW0HGf9IDFN40eAWuuvg,39
|
|
116
|
+
thoughtleaders_cli-0.7.8.dist-info/licenses/LICENSE,sha256=RUfdfLsn6jygiyrnnVUHt6r4IPwr2rbDm9Kixgtu8fo,1071
|
|
117
|
+
thoughtleaders_cli-0.7.8.dist-info/RECORD,,
|
tl_cli/__init__.py
CHANGED
|
@@ -204,6 +204,7 @@ tl recommender tags [query] # List similarity tag names — categorie
|
|
|
204
204
|
tl recommender top-channels "<tag>" # Top channels loaded on a similarity tag (Intelligence)
|
|
205
205
|
tl recommender top-profiles "<tag>" # Top brand profiles loaded on a similarity tag
|
|
206
206
|
tl recommender top-brands "<tag>" # Top brands (deduped from profiles) loaded on a similarity tag
|
|
207
|
+
tl recommender channels-with-tag "<tag>" [--min <v>] # ALL channel IDs scoring >= v on a tag (--min default 0.00001 drops zero-loading channels; paged, enumerates the full set; 1 credit/result; Intelligence)
|
|
207
208
|
tl recommender inspect-channel <ref> # Show a channel's similarity-profile breakdown (Intelligence)
|
|
208
209
|
tl recommender inspect-brand <ref> # Show a brand profile's ideal similarity-profile breakdown (Intelligence)
|
|
209
210
|
tl recommender channels-for-profile <id> # Find channels closest to a brand profile's ideal profile (Intelligence)
|
|
@@ -414,9 +415,9 @@ tl db pg "SELECT b.name, COUNT(*) AS deals
|
|
|
414
415
|
|
|
415
416
|
If unsure about what information to find where, read the [references/postgresql-schema.md](references/postgresql-schema.md) file for instructions. Use just `tl pg schema` to see the entire SQL schema.
|
|
416
417
|
|
|
417
|
-
**PG cost is per-
|
|
418
|
+
**PG cost is per-row.** The credit cost for a `tl db pg` call is its per-row rate — the **sum of the rates of the tables the query touches** (default 1.0/row; some tables are cheaper or dearer) plus a **flat per-row charge** for every expensive column read — times the rows returned. So a join pays for each table it reads, and an expensive column costs its configured value for every row returned. Aggregate queries (`count`/`GROUP BY`) add a surcharge proportional to the estimated rows aggregated. Sensitive columns (e.g. demographics, channel outreach emails) cost more per row. Run `tl describe show db --json` to see the live `pg_pricing` map, and check `usage.credit_rate` / `usage.pricing` in the response envelope after a query to see what your query was actually charged.
|
|
418
419
|
|
|
419
|
-
**Preview cost before running.** Add `--pricing` to estimate a query's cost without executing it: `tl db pg "SELECT … LIMIT 100" --pricing` runs only `EXPLAIN`, prints the
|
|
420
|
+
**Preview cost before running.** Add `--pricing` to estimate a query's cost without executing it: `tl db pg "SELECT … LIMIT 100" --pricing` runs only `EXPLAIN`, prints the per-row rate + per-row breakdown and an upper-bound cost (at the query's LIMIT), and costs a flat 1 credit. Use this before large or expensive-column queries. Works with `--json`. `--pricing` also works on `tl db fb` and `tl db es` — those backends have no per-table or per-column charges, so the estimate is just the flat per-row rate at the row ceiling (`LIMIT` for Firebolt; `size`, or the aggregation doc cap, for Elasticsearch).
|
|
420
421
|
|
|
421
422
|
### Three sources, each authoritative for different things
|
|
422
423
|
|
tl_cli/auth/commands.py
CHANGED
|
@@ -187,9 +187,7 @@ def logout_cmd() -> None:
|
|
|
187
187
|
# the interactive login established. Point the user at Auth0's logout
|
|
188
188
|
# URL so the next `tl auth login` doesn't silently SSO straight back in.
|
|
189
189
|
logout_url = f"https://{get_config().auth0_domain}/logout"
|
|
190
|
-
console.print(
|
|
191
|
-
f"To end your Auth0 browser session, visit: [cyan]{logout_url}[/cyan]"
|
|
192
|
-
)
|
|
190
|
+
console.print(f"To end your Auth0 browser session, visit: [cyan]{logout_url}[/cyan]")
|
|
193
191
|
clear_tokens()
|
|
194
192
|
console.print("[green]Logged out successfully.[/green]")
|
|
195
193
|
|
tl_cli/auth/login.py
CHANGED
|
@@ -129,7 +129,7 @@ def login_device_code() -> StoredTokens:
|
|
|
129
129
|
console.print()
|
|
130
130
|
console.print(f"[bold]And enter the code:[/bold] [cyan bold]{user_code}[/cyan bold]")
|
|
131
131
|
console.print()
|
|
132
|
-
console.print(f"[dim]The code expires in {expires_in // 60} minutes.[/dim]")
|
|
132
|
+
console.print(f"[dim]The code expires in {expires_in // 60} minutes. After you have logged in successfully, please wait until the system is notified.[/dim]")
|
|
133
133
|
|
|
134
134
|
# Poll for token
|
|
135
135
|
deadline = time.time() + expires_in
|
tl_cli/commands/describe.py
CHANGED
|
@@ -124,11 +124,11 @@ def _summarise_modes(credits: dict) -> tuple[str, str, bool]:
|
|
|
124
124
|
- 'free' → "free"
|
|
125
125
|
- 'flat' → "<rate> per call"
|
|
126
126
|
- 'linear-per-result' (one mode) → "<rate> × n (per result)"
|
|
127
|
-
- '
|
|
127
|
+
- 'per-row' (one mode, rate=R) → "R/row"
|
|
128
128
|
- mixed (e.g. channels has detail / history / similar at different rates)
|
|
129
129
|
→ per-mode "<mode> R" joined with commas
|
|
130
130
|
|
|
131
|
-
The typical-cost column uses the n=100 example for
|
|
131
|
+
The typical-cost column uses the n=100 example for per-row/per-result and
|
|
132
132
|
the flat rate for flat. Free shows '-'.
|
|
133
133
|
"""
|
|
134
134
|
modes = _modes_block(credits)
|
|
@@ -163,8 +163,8 @@ def _format_single_mode_label(mode_name: str, payload: dict, *, terse: bool = Fa
|
|
|
163
163
|
return f"{_fmt_credits(rate)}/call" if terse else f"{_fmt_credits(rate)} per call"
|
|
164
164
|
if model == "linear-per-result":
|
|
165
165
|
return f"{_fmt_credits(rate)}×n" if terse else f"{_fmt_credits(rate)} × n (per result)"
|
|
166
|
-
if model == "
|
|
167
|
-
return f"
|
|
166
|
+
if model == "per-row":
|
|
167
|
+
return f"{_fmt_credits(rate)}/row"
|
|
168
168
|
return f"{model} ({_fmt_credits(rate)})"
|
|
169
169
|
|
|
170
170
|
|
|
@@ -256,25 +256,25 @@ def _print_pricing_section(credits: dict) -> None:
|
|
|
256
256
|
"Estimate using the examples above before running with a large limit."
|
|
257
257
|
)
|
|
258
258
|
|
|
259
|
-
# Surface live PG
|
|
260
|
-
# (db resource only).
|
|
261
|
-
|
|
259
|
+
# Surface live PG per-table / per-column pricing when the server included
|
|
260
|
+
# it (db resource only).
|
|
261
|
+
_print_pg_pricing_section(credits.get("pg_pricing"))
|
|
262
262
|
|
|
263
263
|
|
|
264
|
-
def
|
|
265
|
-
"""Render the `credits.
|
|
264
|
+
def _print_pg_pricing_section(pricing: object) -> None:
|
|
265
|
+
"""Render the `credits.pg_pricing` block as a flat dotted-path table.
|
|
266
266
|
|
|
267
267
|
The server emits a three-level nested structure
|
|
268
268
|
``{base: {pg: float}, tables: {name: float}, columns: {"t.c": float}}``;
|
|
269
|
-
flattening each leaf to ``<section>.<key>`` keeps the live
|
|
270
|
-
|
|
271
|
-
from the per-table and per-column extras a query
|
|
272
|
-
incur.
|
|
269
|
+
flattening each leaf to ``<section>.<key>`` keeps the live rates visible
|
|
270
|
+
in one sorted scan, with the default per-row rate (``default.pg``)
|
|
271
|
+
distinguished from the per-table rates and per-column extras a query
|
|
272
|
+
may or may not incur.
|
|
273
273
|
"""
|
|
274
|
-
if not isinstance(
|
|
274
|
+
if not isinstance(pricing, dict) or not pricing:
|
|
275
275
|
return
|
|
276
276
|
rows: list[tuple[str, float]] = []
|
|
277
|
-
for section, body in
|
|
277
|
+
for section, body in pricing.items():
|
|
278
278
|
if not isinstance(body, dict):
|
|
279
279
|
# Forward-compat: an unexpected leaf type — surface as-is
|
|
280
280
|
# under the section name rather than dropping it silently.
|
|
@@ -284,15 +284,16 @@ def _print_pg_expensive_section(expensive: object) -> None:
|
|
|
284
284
|
rows.append((f"{section}.{key}", val))
|
|
285
285
|
if not rows:
|
|
286
286
|
return
|
|
287
|
-
sub = Table(title="PG
|
|
287
|
+
sub = Table(title="PG per-row pricing (live)")
|
|
288
288
|
sub.add_column("Path", style="bold")
|
|
289
|
-
sub.add_column("
|
|
289
|
+
sub.add_column("Rate", justify="right")
|
|
290
290
|
for path, val in sorted(rows):
|
|
291
291
|
sub.add_row(path, _fmt_credits(val))
|
|
292
292
|
console.print(sub)
|
|
293
293
|
console.print(
|
|
294
|
-
"[dim]These are the rates, not a per-query total.
|
|
295
|
-
"of
|
|
294
|
+
"[dim]These are the per-table / per-column rates, not a per-query total. "
|
|
295
|
+
"A query's per-row rate is the sum of the rates of the tables it touches. "
|
|
296
|
+
"For the actual cost of a specific query (before running it), use[/dim] "
|
|
296
297
|
"[cyan]tl db pg \"SELECT ...\" --pricing[/cyan][dim].[/dim]"
|
|
297
298
|
)
|
|
298
299
|
|
tl_cli/commands/recommender.py
CHANGED
|
@@ -210,6 +210,54 @@ def top_brands_cmd(
|
|
|
210
210
|
_do_top("brands", tag, args or [], fmt, limit, TOP_BRAND_COLUMNS, f"Top brands: {tag}")
|
|
211
211
|
|
|
212
212
|
|
|
213
|
+
@app.command("channels-with-tag")
|
|
214
|
+
def channels_with_tag_cmd(
|
|
215
|
+
tag: str = typer.Argument(..., help='Similarity tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
|
|
216
|
+
min_value: float = typer.Option(0.00001, "--min", help="Inclusive minimum tag value; only channels scoring at or above this are returned. Defaults to 0.00001, which excludes channels with no loading on the tag."),
|
|
217
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
218
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
219
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
220
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
221
|
+
limit: int = typer.Option(100, "--limit", "-l", help="Max results per page (1-1000)"),
|
|
222
|
+
offset: int = typer.Option(0, "--offset", help="Pagination offset"),
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Every channel whose value for a similarity tag is at or above a threshold.
|
|
225
|
+
|
|
226
|
+
Unlike `top-channels` (which ranks the strongest few), this walks the
|
|
227
|
+
*entire* match set in pages of up to 1000 — including sets larger than
|
|
228
|
+
the search index's 10k window — so you can enumerate every channel above
|
|
229
|
+
a cutoff. Returns channel IDs only; expand them with `tl channels show`
|
|
230
|
+
or `tl recommender inspect-channel`.
|
|
231
|
+
|
|
232
|
+
`--min` defaults to 0.00001 — just above zero — so a bare call returns
|
|
233
|
+
every channel with any loading on the tag and drops the zero-fill rest.
|
|
234
|
+
Raise it for a stricter cutoff.
|
|
235
|
+
|
|
236
|
+
Costs 1 credit per channel ID returned. Intelligence plan required.
|
|
237
|
+
|
|
238
|
+
Examples:
|
|
239
|
+
tl recommender channels-with-tag "Cooking"
|
|
240
|
+
tl recommender channels-with-tag "Age 18-24" --min 0.3 --limit 1000
|
|
241
|
+
tl recommender channels-with-tag "Cooking" --min 0.5 --offset 1000 --json
|
|
242
|
+
"""
|
|
243
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
244
|
+
tag = _strip_quotes(tag)
|
|
245
|
+
params = {"tag": tag, "min": str(min_value), "limit": str(limit), "offset": str(offset)}
|
|
246
|
+
client = get_client()
|
|
247
|
+
try:
|
|
248
|
+
data = client.get("/recommender/channels-with-tag", params=params)
|
|
249
|
+
output(
|
|
250
|
+
data,
|
|
251
|
+
fmt,
|
|
252
|
+
columns=["channel_id"],
|
|
253
|
+
title=f"Channels with {tag} >= {min_value}",
|
|
254
|
+
)
|
|
255
|
+
except ApiError as e:
|
|
256
|
+
handle_api_error(e)
|
|
257
|
+
finally:
|
|
258
|
+
client.close()
|
|
259
|
+
|
|
260
|
+
|
|
213
261
|
@app.command("inspect-channel")
|
|
214
262
|
def inspect_channel_cmd(
|
|
215
263
|
channel_ref: str = typer.Argument(..., help="Channel ID (numeric) or name (partial match, must be unique)"),
|
tl_cli/output/formatter.py
CHANGED
|
@@ -540,7 +540,7 @@ def output_pricing_estimate(data: dict, fmt: str) -> None:
|
|
|
540
540
|
|
|
541
541
|
Firebolt and Elasticsearch have no per-column extras — their estimate
|
|
542
542
|
carries `per_row_extra=0` and empty expensive-item maps, so the
|
|
543
|
-
breakdown table is skipped and only the
|
|
543
|
+
breakdown table is skipped and only the per-row cost shows.
|
|
544
544
|
A `limit`/cost of `None` (e.g. a Firebolt query with no `LIMIT`) means
|
|
545
545
|
the row count is unbounded and the cost can't be pinned ahead of time.
|
|
546
546
|
"""
|
|
@@ -567,24 +567,30 @@ def output_pricing_estimate(data: dict, fmt: str) -> None:
|
|
|
567
567
|
else:
|
|
568
568
|
console.print(
|
|
569
569
|
" Estimated cost: [yellow]depends on rows returned[/yellow] "
|
|
570
|
-
"(no row limit set — cost
|
|
570
|
+
"(no row limit set — cost is linear in rows returned)"
|
|
571
571
|
)
|
|
572
|
-
console.print(f"
|
|
572
|
+
console.print(f" Per-row rate (sum of table rates): {multiplier}")
|
|
573
573
|
console.print(f" Per-row extra (expensive columns): {per_row}")
|
|
574
|
+
agg_surcharge = est.get("agg_surcharge")
|
|
575
|
+
if agg_surcharge:
|
|
576
|
+
console.print(
|
|
577
|
+
f" Aggregate surcharge (flat): {agg_surcharge} "
|
|
578
|
+
f"[dim](≈{est.get('aggregated_rows', 0):,} rows aggregated)[/dim]"
|
|
579
|
+
)
|
|
574
580
|
if planner_rows is not None:
|
|
575
581
|
console.print(
|
|
576
582
|
f" [dim]Planner row estimate (pre-LIMIT): {planner_rows:,}[/dim]"
|
|
577
583
|
)
|
|
578
584
|
|
|
579
|
-
tables = est.get("
|
|
585
|
+
tables = est.get("table_rates") or {}
|
|
580
586
|
columns = est.get("expensive_columns") or {}
|
|
581
587
|
if tables or columns:
|
|
582
|
-
sub = Table(title="
|
|
588
|
+
sub = Table(title="Per-row rates this query touches")
|
|
583
589
|
sub.add_column("Item", style="bold")
|
|
584
590
|
sub.add_column("Kind")
|
|
585
|
-
sub.add_column("
|
|
591
|
+
sub.add_column("Rate", justify="right")
|
|
586
592
|
for name, val in sorted(tables.items()):
|
|
587
|
-
sub.add_row(name, "table (
|
|
593
|
+
sub.add_row(name, "table (per row)", f"{_fmt_credits(val)}/row")
|
|
588
594
|
for path, val in sorted(columns.items()):
|
|
589
595
|
sub.add_row(path, "column (per row)", f"{_fmt_credits(val)}/row")
|
|
590
596
|
console.print(sub)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|