thoughtleaders-cli 0.6.5__tar.gz → 0.6.7__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.
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/AGENTS.md +2 -2
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/PKG-INFO +6 -6
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/README.md +5 -5
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/agents/tl-analyst.md +2 -2
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/docs/architecture.md +2 -2
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/SKILL.md +48 -23
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/elasticsearch-schema.md +2 -4
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/firebolt-schema.md +1 -1
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/postgres-schema.md +2 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/brands.py +46 -2
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/channels.py +4 -4
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/recommender.py +77 -28
- thoughtleaders_cli-0.6.7/src/tl_cli/commands/schema.py +83 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/main.py +0 -60
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/uv.lock +1 -1
- thoughtleaders_cli-0.6.5/src/tl_cli/commands/schema.py +0 -55
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/.gitignore +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/LICENSE +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/commands/tl-balance.md +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/commands/tl-reports.md +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/commands/tl-sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/commands/tl.md +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/ask.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/comments.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/tests/test_sponsorships.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Project Overview
|
|
2
2
|
|
|
3
|
-
**tl-cli** is a Python CLI for querying ThoughtLeaders sponsorship data (sponsorships, channels, brands, uploads, snapshots, reports,
|
|
3
|
+
**tl-cli** is a Python CLI for querying ThoughtLeaders sponsorship data (sponsorships, channels, brands, uploads, snapshots, reports, recommender). Built with Typer + Rich + httpx. Designed as an "agent-first tool" — the CLI handles structured commands and output, while the user's AI agent (Claude) provides intelligence.
|
|
4
4
|
|
|
5
5
|
# Architecture
|
|
6
6
|
|
|
@@ -20,7 +20,7 @@ When adding a new data command, follow this pattern. See `sponsorships.py` for t
|
|
|
20
20
|
|
|
21
21
|
`deals`, `matches`, and `proposals` are shortcut commands that delegate to sponsorships' `do_list`/`do_show`/`do_create` with a pre-set status filter. They reject explicit `status:` filters — users should use `tl sponsorships list` for finer-grained status filtering.
|
|
22
22
|
|
|
23
|
-
`recommender` (`commands/recommender.py`) wraps the
|
|
23
|
+
`recommender` (`commands/recommender.py`) wraps the recommender API at `/api/cli/v1/recommender/*` — `tags` (free), `top-channels` / `top-profiles` / `top-brands`, `inspect-channel`, `inspect-brand`, `similar-to-profile` (all 25 credits flat, Intelligence-gated). The three `top-*` URLs share one server resolver; `top-brands` dedupes the underlying profile rows by brand. Channel→channel and brand→brand similarity stay on `tl channels similar` / `tl brands similar`. When updating the SKILL or examples, prefer steering category/topic discovery (e.g. "Cooking channels") to `tl recommender top-channels "<tag>"` rather than `WHERE content_category = <code>` SQL — the recommender is ranked, not equality-based. The underlying recommender code uses "element"/"field_name" terminology; the CLI/API layer renames these to "tag" at the boundary.
|
|
24
24
|
|
|
25
25
|
## Filter Parsing (`filters.py`)
|
|
26
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.7
|
|
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
|
|
@@ -83,7 +83,7 @@ tl uploads show 1174310:0BehkmVa7ak
|
|
|
83
83
|
|
|
84
84
|
# Search channels via raw SQL — `tl db pg` against thoughtleaders_channel
|
|
85
85
|
# (run `tl schema pg` once to confirm the live column set).
|
|
86
|
-
# NOTE: For topic / category discovery, prefer the
|
|
86
|
+
# NOTE: For topic / category discovery, prefer the recommender over
|
|
87
87
|
# `content_category` equality — `tl recommender top-channels "<tag>"`
|
|
88
88
|
# returns channels ranked by how strongly they load on the topic, not just
|
|
89
89
|
# rows where the single category code matches exactly.
|
|
@@ -101,22 +101,22 @@ tl db pg "SELECT id, channel_name FROM thoughtleaders_channel
|
|
|
101
101
|
tl channels show 12345
|
|
102
102
|
tl channels show "Economics Explained"
|
|
103
103
|
|
|
104
|
-
# Find similar channels (
|
|
104
|
+
# Find similar channels (recommender, 25 credits, Intelligence plan).
|
|
105
105
|
# msn: is tri-state (default msn:yes): yes = MSN only, no = non-MSN only, both = no filter.
|
|
106
106
|
# tpp: is tri-state (default tpp:both): yes = TPP only, no = non-TPP only, both = no filter.
|
|
107
107
|
# Same ID-or-name resolution rules as `channels show`.
|
|
108
108
|
tl channels similar 12345 --limit 10
|
|
109
109
|
tl channels similar "Tremending girls" min-score:0.85 --limit 5
|
|
110
110
|
|
|
111
|
-
#
|
|
112
|
-
# `tags` is free; `top
|
|
111
|
+
# Recommender — discovery by category/demographic tag (Intelligence plan).
|
|
112
|
+
# `tags` is free; `top-*`, `inspect-*`, `similar-to-profile`, and `similar-brands-to-channel` cost 25 credits flat.
|
|
113
113
|
tl recommender tags # List every tag (free)
|
|
114
114
|
tl recommender tags cooking # Search tag names by substring
|
|
115
115
|
tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
|
|
116
116
|
tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
|
|
117
117
|
tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
|
|
118
118
|
tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
|
|
119
|
-
tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal
|
|
119
|
+
tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal profile
|
|
120
120
|
tl recommender similar-to-profile 842 # Channels closest to a brand profile
|
|
121
121
|
|
|
122
122
|
# Brand intelligence
|
|
@@ -56,7 +56,7 @@ tl uploads show 1174310:0BehkmVa7ak
|
|
|
56
56
|
|
|
57
57
|
# Search channels via raw SQL — `tl db pg` against thoughtleaders_channel
|
|
58
58
|
# (run `tl schema pg` once to confirm the live column set).
|
|
59
|
-
# NOTE: For topic / category discovery, prefer the
|
|
59
|
+
# NOTE: For topic / category discovery, prefer the recommender over
|
|
60
60
|
# `content_category` equality — `tl recommender top-channels "<tag>"`
|
|
61
61
|
# returns channels ranked by how strongly they load on the topic, not just
|
|
62
62
|
# rows where the single category code matches exactly.
|
|
@@ -74,22 +74,22 @@ tl db pg "SELECT id, channel_name FROM thoughtleaders_channel
|
|
|
74
74
|
tl channels show 12345
|
|
75
75
|
tl channels show "Economics Explained"
|
|
76
76
|
|
|
77
|
-
# Find similar channels (
|
|
77
|
+
# Find similar channels (recommender, 25 credits, Intelligence plan).
|
|
78
78
|
# msn: is tri-state (default msn:yes): yes = MSN only, no = non-MSN only, both = no filter.
|
|
79
79
|
# tpp: is tri-state (default tpp:both): yes = TPP only, no = non-TPP only, both = no filter.
|
|
80
80
|
# Same ID-or-name resolution rules as `channels show`.
|
|
81
81
|
tl channels similar 12345 --limit 10
|
|
82
82
|
tl channels similar "Tremending girls" min-score:0.85 --limit 5
|
|
83
83
|
|
|
84
|
-
#
|
|
85
|
-
# `tags` is free; `top
|
|
84
|
+
# Recommender — discovery by category/demographic tag (Intelligence plan).
|
|
85
|
+
# `tags` is free; `top-*`, `inspect-*`, `similar-to-profile`, and `similar-brands-to-channel` cost 25 credits flat.
|
|
86
86
|
tl recommender tags # List every tag (free)
|
|
87
87
|
tl recommender tags cooking # Search tag names by substring
|
|
88
88
|
tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
|
|
89
89
|
tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
|
|
90
90
|
tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
|
|
91
91
|
tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
|
|
92
|
-
tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal
|
|
92
|
+
tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal profile
|
|
93
93
|
tl recommender similar-to-profile 842 # Channels closest to a brand profile
|
|
94
94
|
|
|
95
95
|
# Brand intelligence
|
|
@@ -16,7 +16,7 @@ For anything beyond a trivially simple lookup, write a single raw query against
|
|
|
16
16
|
- **Elasticsearch (`tl db es`)** — transcript / brand-mention text search, video-level aggregations, demographic country-share filters that compose with content predicates.
|
|
17
17
|
- **Firebolt (`tl db fb`)** — custom time-series shapes (multi-channel growth comparisons, milestone-age slices). For default shapes, prefer `tl snapshots`.
|
|
18
18
|
|
|
19
|
-
Reserve structured commands for: single-record `show` by ID, plain filtered `list` with one or two filters that the structured vocabulary already supports, `tl channels similar` / `tl brands similar` (
|
|
19
|
+
Reserve structured commands for: single-record `show` by ID, plain filtered `list` with one or two filters that the structured vocabulary already supports, `tl channels similar` / `tl brands similar` (similarity search), `tl reports run`, and `tl snapshots`.
|
|
20
20
|
|
|
21
21
|
One raw query beats N paginated structured walks stitched in `jq`/Python — on cost, latency, and the ES `from+size = 10000` cap.
|
|
22
22
|
|
|
@@ -69,7 +69,7 @@ Then suggest `tl comments add <id> "..."` for each.
|
|
|
69
69
|
### Multi-step research (mix raw + similarity)
|
|
70
70
|
"Find channels similar to the ones Nike sponsors and compare their pricing"
|
|
71
71
|
1. `tl db pg` to find the top channels Nike has sponsored (one aggregation, ranked).
|
|
72
|
-
2. `tl channels similar <top-channel-id> --json --limit 20` per seed —
|
|
72
|
+
2. `tl channels similar <top-channel-id> --json --limit 20` per seed — similarity search is server-side and has no SQL equivalent. The `msn:` filter is tri-state with default `msn:yes` (MSN channels only); use `msn:both` to broaden, `msn:no` for non-MSN only.
|
|
73
73
|
3. Union + dedupe + compile comparison table.
|
|
74
74
|
|
|
75
75
|
### Report comparison (saved reports)
|
|
@@ -50,10 +50,10 @@ All data commands use explicit subcommands: `list`, `show`, `create`/`add`. Runn
|
|
|
50
50
|
| `tl uploads show <id> [<id>...]` | Show upload detail(s) by ID |
|
|
51
51
|
| `tl channels show <id-or-name>` | Channel detail, including active adspots with price/cost/CPM |
|
|
52
52
|
| `tl channels history <id-or-name>` | Sponsorship history (videos with detected sponsors) |
|
|
53
|
-
| `tl channels similar <id-or-name>` |
|
|
53
|
+
| `tl channels similar <id-or-name>` | Similarity recommender. 25 credits; Intelligence plan. Tri-state `msn:` (default `yes`) and `tpp:` (default `both`) filters. Ambiguous names return 400 + candidates list. Hidden `look-alike` alias. |
|
|
54
54
|
| `tl brands show <brand>` | Brand intelligence report |
|
|
55
55
|
| `tl brands history <brand> [--channel <id>]` | Brand sponsorship history; videos where the brand was detected |
|
|
56
|
-
| `tl brands similar <brand>` | Find similar brands (
|
|
56
|
+
| `tl brands similar <brand>` | Find similar brands (similarity search, 25 credits) |
|
|
57
57
|
| `tl snapshots channel <id>` | Channel metrics over time (Firebolt channel_metrics) |
|
|
58
58
|
| `tl snapshots video <id> --channel <id>` | Video view curve (Firebolt article_metrics, --channel required) |
|
|
59
59
|
| `tl comments list <adlink-id>` | List comments on a sponsorship (free) |
|
|
@@ -20,6 +20,8 @@ Decision rule:
|
|
|
20
20
|
|
|
21
21
|
Always run `tl describe show <resource>` before using a structured command, and `tl schema pg|fb|es` before writing a raw query.
|
|
22
22
|
|
|
23
|
+
**When you only need the schema of one table, you MUST call `tl schema pg <table>` (or `tl schema fb <table>`) — never the unscoped form.** The unscoped `tl schema pg` returns *every* table visible to your role, which is dozens of tables and tens of thousands of tokens; the single-table form returns only the section you need, in the same markdown layout. Reaching for the unscoped form when you already know the table name is a tokens/latency tax with zero benefit. ES has no per-table form (the index is a single document shape) — `tl schema es` is the only call there.
|
|
24
|
+
|
|
23
25
|
**Process data with shell tools, not your context window.** Don't pull large result sets into your reasoning context just to filter, sort, count, or extract a field — that wastes tokens and slows you down. Pipe `tl … --json` (or `--csv`) into `jq`, `yq`, `rg`, or `duckdb` and read only the answer back. Pick the tool by shape:
|
|
24
26
|
|
|
25
27
|
- **`jq`** — filter, project, and transform JSON. The default for `tl … --json` post-processing.
|
|
@@ -67,6 +69,7 @@ Other key concepts:
|
|
|
67
69
|
- **Comments** — notes attached to sponsorships
|
|
68
70
|
- **Adspots** — types of ads a channel carries (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost.
|
|
69
71
|
- **MSN** (Media Selling Network) — the ~11k YouTube channels that have opted in to receive sponsorship offers. A channels is in the MSN group if the `channel.media_selling_network_join_date` field is not null.
|
|
72
|
+
- **MBN** (Media Buying Network) — the brand-side counterpart to MSN: brand profiles that have opted in to receive proposed sponsorships. A profile is in the MBN group if the `profile.media_buying_network_join_date` field is not null.
|
|
70
73
|
- **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the smaller, exclusive ~169 channels TL manages directly. A channel is in the TPP group if the `channel.is_tl_channel` is True.
|
|
71
74
|
- **`demographics_updated_at`** (on channel detail) — ISO timestamp of when demographic screenshots were last uploaded and processed via OCR. If non-null, the channel has demographics screenshots on file. If null, no screenshots have been uploaded. Use this to check whether a channel has demographics data from screenshots.
|
|
72
75
|
- **`impression`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed.
|
|
@@ -95,11 +98,29 @@ When querying sponsorship bookings, query by `status:sold` and filter the the da
|
|
|
95
98
|
|
|
96
99
|
Where possible, if searching for a sponsorship match between channels and brands, first search for what do similar brands sponsor / which brands is the channel usually sponsored by. The similarity judgement should be preferably based on similar topics, similar upload frequency, similar channel sizes, and only after all that, on demographics.
|
|
97
100
|
|
|
98
|
-
Use the `tl channels similar` and `tl brands similar` commands to explore 1:1 similarity between known channels or brands. For category- or topic-driven discovery (e.g. "find me Cooking channels", "who scores high on USA share?"), use `tl recommender top-channels "<tag>"` (or `top-brands`/`top-profiles`) against the
|
|
101
|
+
Use the `tl channels similar` and `tl brands similar` commands to explore 1:1 similarity between known channels or brands. For category- or topic-driven discovery (e.g. "find me Cooking channels", "who scores high on USA share?"), use `tl recommender top-channels "<tag>"` (or `top-brands`/`top-profiles`) against the recommender — that's faster, ranked by category-strength. Run `tl recommender tags` to discover the valid tag names.
|
|
99
102
|
|
|
100
103
|
## Workflow
|
|
101
104
|
|
|
102
|
-
At the start of session, always run
|
|
105
|
+
At the start of session, always run `tl --help` to find out which command groups are available, and `tl whoami` to find out what you have access to.
|
|
106
|
+
|
|
107
|
+
### How to discover commands and subcommands
|
|
108
|
+
|
|
109
|
+
The CLI exposes three different discovery surfaces — pick by what you actually need:
|
|
110
|
+
|
|
111
|
+
| You want to know… | Run |
|
|
112
|
+
|---|---|
|
|
113
|
+
| Top-level command groups (`sponsorships`, `channels`, `db`, `recommender`, etc.) | `tl --help` |
|
|
114
|
+
| Subcommands of a group (`tl recommender` → `tags`, `top-channels`, `inspect-brand`, …) | `tl <group> --help` (e.g. `tl recommender --help`, `tl db --help`) |
|
|
115
|
+
| Arguments and flags for a specific leaf command | `tl <group> <subcommand> --help` (e.g. `tl recommender top-channels --help`) |
|
|
116
|
+
| Fields, filters, credit rates for a **data resource** (sponsorships, uploads, snapshots, reports, comments, recommender) | `tl describe show <resource> --json` |
|
|
117
|
+
| The live PG/ES/Firebolt schema for raw `tl db` queries | `tl schema pg` / `tl schema es` / `tl schema fb` |
|
|
118
|
+
| The schema of a **single** PG / Firebolt table | **`tl schema pg <table>`** / **`tl schema fb <table>`** — strongly preferred when you only need one |
|
|
119
|
+
|
|
120
|
+
Notes:
|
|
121
|
+
- Use `--help` everywhere — there is no separate `tl help` command. `tl help` returns "No such command 'help'".
|
|
122
|
+
- **`tl describe show channels`** and **`tl describe show brands`** intentionally do not list fields/filters — channel and brand search live in raw SQL (`tl db pg`) and the recommender, not in a structured list endpoint. They print a notice steering you there.
|
|
123
|
+
- `--help` describes **CLI shape**; `tl describe` describes **data shape**. They don't overlap.
|
|
103
124
|
|
|
104
125
|
Unless the user specifically asks for running a specific report or showing the result of a specific report, find the data by using other, low-level commands.
|
|
105
126
|
|
|
@@ -132,18 +153,20 @@ tl uploads list [filters...] # Video uploads from ES — list curve, m
|
|
|
132
153
|
tl uploads show <id> # Upload detail (2 credits)
|
|
133
154
|
tl channels show <id-or-name> # Channel detail (2 credits; accepts numeric ID or name) — for channel search use raw SQL on thoughtleaders_channel
|
|
134
155
|
tl channels history <id-or-name> # Sponsorship history (5 credits/result, linear)
|
|
135
|
-
tl channels similar <id-or-name> #
|
|
156
|
+
tl channels similar <id-or-name> # Similarity recommender (25 credits flat; Intelligence plan)
|
|
136
157
|
tl brands show <id-or-name> # Brand detail (1 credit)
|
|
137
158
|
tl brands history <id-or-name> # Sponsorship history (5 credits/result, linear)
|
|
138
159
|
tl brands history <query> --channel <id> # Brand mentions on specific channel
|
|
139
|
-
tl brands
|
|
140
|
-
tl
|
|
141
|
-
tl
|
|
142
|
-
tl recommender
|
|
143
|
-
tl recommender top-
|
|
144
|
-
tl recommender
|
|
145
|
-
tl recommender
|
|
146
|
-
tl recommender
|
|
160
|
+
tl brands history-stats <id-or-name> # Aggregate roll-up: counts, total/avg/median views, first/last seen, by-year, top channels (5 credits flat)
|
|
161
|
+
tl brands history-stats <q> --channel <id> # Same roll-up, narrowed to one channel
|
|
162
|
+
tl brands similar <id-or-name> # Find similar brands via similarity search (25 credits flat)
|
|
163
|
+
tl recommender tags [query] # List similarity tag names — categories, demographics, formats (free)
|
|
164
|
+
tl recommender top-channels "<tag>" # Top channels loaded on a similarity tag (25 credits; Intelligence)
|
|
165
|
+
tl recommender top-profiles "<tag>" # Top brand profiles loaded on a similarity tag (25 credits)
|
|
166
|
+
tl recommender top-brands "<tag>" # Top brands (deduped from profiles) loaded on a similarity tag (25 credits)
|
|
167
|
+
tl recommender inspect-channel <ref> # Show a channel's similarity-profile breakdown (25 credits; Intelligence)
|
|
168
|
+
tl recommender inspect-brand <ref> # Show a brand profile's ideal similarity-profile breakdown (25 credits; Intelligence)
|
|
169
|
+
tl recommender similar-to-profile <id> # Channels closest to a brand profile's ideal profile (25 credits; Intelligence)
|
|
147
170
|
tl snapshots channel <id> # Channel metrics over time — list curve, mult 1.2 (Firebolt-backed)
|
|
148
171
|
tl snapshots video <id> --channel <id> # Video view curve — list curve, mult 1.2 (--channel required!)
|
|
149
172
|
tl reports # List saved reports — list curve, mult 1.3
|
|
@@ -196,7 +219,7 @@ Structured commands are still the right tool for: single-record `show` by ID, pl
|
|
|
196
219
|
| Custom Firebolt shape (milestone-age slices, multi-channel growth comparisons) | **`tl db fb`** |
|
|
197
220
|
| Single-record detail lookup by ID | `tl <resource> show <id>` |
|
|
198
221
|
| Plain filtered list with one or two simple filters | `tl <resource> list` |
|
|
199
|
-
| Channel/brand similarity (
|
|
222
|
+
| Channel/brand similarity (server-implemented similarity search) | `tl channels similar`, `tl brands similar` |
|
|
200
223
|
| Saved reports | `tl reports`, `tl reports run` |
|
|
201
224
|
| Time-series view-curve / channel growth (default shape with interpolation) | `tl snapshots channel`, `tl snapshots video` |
|
|
202
225
|
|
|
@@ -289,7 +312,7 @@ See [references/business-glossary.md](references/business-glossary.md) for reven
|
|
|
289
312
|
| **AdLink INSERT** with custom price/cost/owner/`weighted_price`/`created_where` | **Unavailable** — `tl sponsorships create` exists but only creates a free *proposal* between a channel and a brand. The `tl db pg` sanitizer accepts SELECT only — no INSERT/UPDATE. | Done in the app or by a human with DB access. |
|
|
290
313
|
| Pre-insert validation queries (joining `adspot ↔ channel ↔ profile ↔ org` to confirm MSN, integration=1, persona, plan) | **Available** via `tl db pg`. | One SELECT joining the four tables. Use `thoughtleaders_channel.media_selling_network_join_date IS NOT NULL` for MSN, `thoughtleaders_adspot.integration = 1` for mention adspots, `thoughtleaders_profile.persona` for the persona code (see persona constants in `references/postgres-schema.md`). |
|
|
291
314
|
| Firebolt cross-table or join queries; filtering on non-indexed columns in WHERE | **Unavailable** — not accepted. | Fetch a wider slice keyed on `channel_id` (and optionally `id`), filter the rest in `jq`/Python. |
|
|
292
|
-
| ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins; any `script_*`; multiple aggregations in one body | **Unavailable** — not accepted. | Rewrite using `term`/`terms`/`match`/`bool`/`nested`. For multi-agg dashboards, run multiple `tl db es` calls and combine client-side. For "similar"-style queries, try `tl channels similar` / `tl brands similar` (
|
|
315
|
+
| ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins; any `script_*`; multiple aggregations in one body | **Unavailable** — not accepted. | Rewrite using `term`/`terms`/`match`/`bool`/`nested`. For multi-agg dashboards, run multiple `tl db es` calls and combine client-side. For "similar"-style queries, try `tl channels similar` / `tl brands similar` (server-implemented similarity search). |
|
|
293
316
|
| ES deep pagination beyond `from+size = 10,000` | **Unavailable** via raw — `scroll` and `pit` aren't allowlisted; `search_after` is allowed but `from` is still capped. | Use `search_after` with `sort` to walk past 10k. For huge sweeps, narrow with `publication_date` ranges. |
|
|
294
317
|
| ES index introspection (`_cat/indices`, mappings) | **Unavailable** — only `_search` is wired. | Read [references/elasticsearch-schema.md](references/elasticsearch-schema.md). It's manually maintained — update it when you discover new fields. |
|
|
295
318
|
| Schema introspection on Postgres (`information_schema.columns`, `pg_class`, …) | **Partial** — catalog-resolving casts and many `pg_*` helpers are blocked. | Use `tl schema pg` for the live table/column listing, or read [references/postgres-schema.md](references/postgres-schema.md). |
|
|
@@ -300,8 +323,10 @@ If a user asks for one of the **Unavailable** items, say so explicitly and propo
|
|
|
300
323
|
```bash
|
|
301
324
|
tl describe # List all resources with credit costs (free)
|
|
302
325
|
tl describe show <resource> --json # Fields, filters, credit rates (free)
|
|
303
|
-
tl schema pg # PostgreSQL schema reference for `tl db pg` (free)
|
|
304
|
-
tl schema
|
|
326
|
+
tl schema pg # PostgreSQL schema reference for `tl db pg` (free) — every visible table
|
|
327
|
+
tl schema pg <table> # PostgreSQL schema for a SINGLE table (free) — same markdown shape
|
|
328
|
+
tl schema fb # Live Firebolt tables and column types for `tl db fb` (free) — both tables
|
|
329
|
+
tl schema fb <table> # Firebolt schema for a SINGLE table (free) — `article_metrics` or `channel_metrics`
|
|
305
330
|
tl schema es # Elasticsearch document shape for `tl db es` (free)
|
|
306
331
|
tl balance --json # Credit balance (free)
|
|
307
332
|
tl whoami # Current user, org, brands (free)
|
|
@@ -325,14 +350,14 @@ Date filters accept keywords: `today`, `yesterday`, `tomorrow`.
|
|
|
325
350
|
|
|
326
351
|
#### Channel discovery — recommender first, raw SQL second
|
|
327
352
|
|
|
328
|
-
For category- or demographic-driven discovery, **use the
|
|
353
|
+
For category- or demographic-driven discovery, **use the recommender, not `content_category` SQL.** The recommender ranks channels by how strongly they load on a category/demographic tag (similarity scores), instead of forcing exact equality on a single integer code. It also returns the matching brand profiles alongside the channels — useful when the user actually wants to know "who buys this kind of inventory."
|
|
329
354
|
|
|
330
355
|
```bash
|
|
331
356
|
# Discover the right tag name first (free)
|
|
332
357
|
tl recommender tags cooking
|
|
333
358
|
tl recommender tags "usa"
|
|
334
359
|
|
|
335
|
-
# Top channels & profiles loaded on a
|
|
360
|
+
# Top channels & profiles loaded on a similarity tag (25 credits; Intelligence)
|
|
336
361
|
tl recommender top-channels "Cooking" msn:yes --limit 50
|
|
337
362
|
tl recommender top-channels "Tech" --limit 30
|
|
338
363
|
tl recommender top-brands "USA share" mbn:yes --limit 50
|
|
@@ -434,7 +459,7 @@ tl reports run 42 --json
|
|
|
434
459
|
|
|
435
460
|
"Find Cooking channels with US-heavy mobile audiences":
|
|
436
461
|
```bash
|
|
437
|
-
# Use the
|
|
462
|
+
# Use the recommender for the topic, then narrow with structured filters / SQL on the IDs.
|
|
438
463
|
tl recommender top-channels "Cooking" msn:yes --limit 100 --json \
|
|
439
464
|
| jq -r '.results[].channel_id' \
|
|
440
465
|
| paste -sd, - \
|
|
@@ -452,7 +477,7 @@ tl recommender top-channels "Cooking" msn:yes --limit 100 --json \
|
|
|
452
477
|
tl sponsorships list status:sold primary-device:mobile min-us-share:60 --json
|
|
453
478
|
```
|
|
454
479
|
|
|
455
|
-
"Find channels similar to one I know" (
|
|
480
|
+
"Find channels similar to one I know" (similarity recommender, 25 credits per call):
|
|
456
481
|
```bash
|
|
457
482
|
tl channels similar 29834 --limit 10 # by ID (defaults to msn:yes, tpp:both)
|
|
458
483
|
tl channels similar "Tremending girls" --limit 5 # by unique name
|
|
@@ -464,16 +489,16 @@ tl channels similar 29834 min-subs:1000000 exclude:477487 --limit 15 # client-s
|
|
|
464
489
|
```
|
|
465
490
|
**Both `tl channels show` and `tl channels similar` accept either a numeric channel ID or a channel name.** Name arguments are case-insensitive partial matches; if more than one active channel matches, the command prints a candidates table (channel_id, subscribers, name) and exits 1 so you can retry with a specific ID. The `msn` filter on `similar` is tri-state: `yes` (only MSN channels — the default), `no` (only non-MSN channels), `both` (no MSN filter). `tl channels look-alike` is a hidden alias for `similar` that matches the internal "look-alike channels" terminology.
|
|
466
491
|
|
|
467
|
-
"Browse the
|
|
492
|
+
"Browse the recommender" (categories, demographics, formats — `tl recommender tags` is free):
|
|
468
493
|
```bash
|
|
469
494
|
tl recommender tags # Full tag list (free)
|
|
470
495
|
tl recommender tags cooking # Search tag names by substring
|
|
471
|
-
tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels loaded on a tag (
|
|
496
|
+
tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels loaded on a tag (25 credits)
|
|
472
497
|
tl recommender top-profiles "Cooking" --limit 30 # Top brand profiles for the tag
|
|
473
498
|
tl recommender top-brands "USA share" mbn:yes --limit 30 # Top brands (deduped) — demographic tag, MBN only
|
|
474
499
|
tl recommender top-channels "Tech" exclude-for-profile:842 # Drop channels already proposed for profile 842
|
|
475
500
|
tl recommender inspect-channel 29834 # Per-tag breakdown of a channel's vector
|
|
476
|
-
tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal
|
|
477
|
-
tl recommender similar-to-profile 842 --limit 30 # Channels closest to a brand profile's ideal
|
|
501
|
+
tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal profile
|
|
502
|
+
tl recommender similar-to-profile 842 --limit 30 # Channels closest to a brand profile's ideal profile
|
|
478
503
|
```
|
|
479
504
|
Use `tl recommender top` for category/topic discovery (it's ranked) and `tl channels similar` / `tl brands similar` for 1:1 lookalike searches.
|
{thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
@@ -158,11 +158,9 @@ The full table below applies to **channel parent docs only**:
|
|
|
158
158
|
### Other indices
|
|
159
159
|
|
|
160
160
|
- `tl-ingest` — ingestion queue. **Don't query.** Internal pipeline state.
|
|
161
|
-
- `tl-
|
|
161
|
+
- `tl-similarity-profiles-channel`, `tl-similarity-profiles-channel-profile` — channel similarity vectors.
|
|
162
162
|
- `tl-vectors-brand-company-descriptions-*` — brand similarity vectors.
|
|
163
|
-
- `tl-vectors-channel-audience-*`, `tl-vectors-channel-topic-descriptions-*`, `tl-vectors-channel-features` — channel
|
|
164
|
-
|
|
165
|
-
Note: `knn` queries against vector indices are **not currently accepted** as a top-level key. For "find similar" results, use `tl channels similar` / `tl brands similar` — they wrap the vector search server-side.
|
|
163
|
+
- `tl-vectors-channel-audience-*`, `tl-vectors-channel-topic-descriptions-*`, `tl-vectors-channel-features` — channel similarity profiles.
|
|
166
164
|
|
|
167
165
|
## Common Query Patterns
|
|
168
166
|
|
{thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/firebolt-schema.md
RENAMED
|
@@ -116,7 +116,7 @@ Every Firebolt workflow has two steps:
|
|
|
116
116
|
**Step 1 — get `channel_id` and (optionally) video IDs from PG/ES.**
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
|
-
# Channels matching some category (
|
|
119
|
+
# Channels matching some category (recommender — preferred over content_category equality)
|
|
120
120
|
tl recommender top-channels "Tech" msn:yes --limit 50 --json \
|
|
121
121
|
| jq '.results[].channel_id'
|
|
122
122
|
|
{thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/postgres-schema.md
RENAMED
|
@@ -27,6 +27,8 @@ The main deals table. Each row = one sponsorship deal between a brand and a YouT
|
|
|
27
27
|
> - ❌ `organization_id` — there is NO direct org FK. Org is reached via `creator_profile_id → profile.organization_id → organization`.
|
|
28
28
|
> - ❌ `channel_id` — channel is reached via `ad_spot_id → adspot.channel_id → channel`.
|
|
29
29
|
> - ❌ `youtube_id` (on channel) — use `external_channel_id`.
|
|
30
|
+
> - ❌ `msn_join_date` (on channel) — use `media_selling_network_join_date`.
|
|
31
|
+
> - ❌ `mbn_join_date` (on profile) — use `media_buying_network_join_date`.
|
|
30
32
|
|
|
31
33
|
#### Key Columns
|
|
32
34
|
|
|
@@ -118,6 +118,50 @@ def history_cmd(
|
|
|
118
118
|
client.close()
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
@app.command("history-stats")
|
|
122
|
+
def history_stats_cmd(
|
|
123
|
+
query: str = typer.Argument(..., help="Brand name or numeric ID"),
|
|
124
|
+
channel: int | None = typer.Option(None, "--channel", "-c", help="Restrict the roll-up to a specific channel"),
|
|
125
|
+
top_channels: int = typer.Option(10, "--top-channels", help="How many top-by-count channels to include in the roll-up (1-50)"),
|
|
126
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
127
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
128
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
129
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Aggregate roll-up of a brand's sponsorship history (no per-row output).
|
|
132
|
+
|
|
133
|
+
Same scope as `tl brands history` (videos where the brand is in
|
|
134
|
+
`sponsored_brand_mentions`), but returned as a single summary
|
|
135
|
+
record: total sponsored videos, view sums/avg/median, first/last
|
|
136
|
+
seen dates, per-year buckets, top channels by count, and
|
|
137
|
+
TL-brokered adlink counts. Computed via one ES aggregation +
|
|
138
|
+
one PG count — cost is flat regardless of how prolific the
|
|
139
|
+
brand is.
|
|
140
|
+
|
|
141
|
+
Requires an Intelligence plan. Costs 5 credits flat.
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
tl brands history-stats Nike
|
|
145
|
+
tl brands history-stats 21416 --top-channels 25
|
|
146
|
+
tl brands history-stats Nike --channel 12345 --json
|
|
147
|
+
"""
|
|
148
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
149
|
+
|
|
150
|
+
params: dict[str, str] = {"top-channels": str(top_channels)}
|
|
151
|
+
if channel is not None:
|
|
152
|
+
params["channel_id"] = str(channel)
|
|
153
|
+
|
|
154
|
+
encoded_query = urllib.parse.quote(query, safe="")
|
|
155
|
+
client = get_client()
|
|
156
|
+
try:
|
|
157
|
+
data = client.get(f"/brands/{encoded_query}/history-stats", params=params)
|
|
158
|
+
output_single(data, fmt)
|
|
159
|
+
except ApiError as e:
|
|
160
|
+
handle_api_error(e)
|
|
161
|
+
finally:
|
|
162
|
+
client.close()
|
|
163
|
+
|
|
164
|
+
|
|
121
165
|
SIMILAR_COLUMNS = ["score", "brand_id", "brand_name", "website", "mbn"]
|
|
122
166
|
SIMILAR_COLUMN_CONFIG = {
|
|
123
167
|
"score": {"justify": "right"},
|
|
@@ -125,7 +169,7 @@ SIMILAR_COLUMN_CONFIG = {
|
|
|
125
169
|
|
|
126
170
|
|
|
127
171
|
def _format_score(results: list[dict]) -> list[dict]:
|
|
128
|
-
"""Convert raw
|
|
172
|
+
"""Convert raw similarity score (0.0-1.0) to percentage string."""
|
|
129
173
|
for row in results:
|
|
130
174
|
score = row.get("score")
|
|
131
175
|
if isinstance(score, (int, float)):
|
|
@@ -144,7 +188,7 @@ def similar_cmd(
|
|
|
144
188
|
) -> None:
|
|
145
189
|
"""Find brands similar to a given one (by ID or name).
|
|
146
190
|
|
|
147
|
-
Costs
|
|
191
|
+
Costs 25 credits per call. Intelligence plan required.
|
|
148
192
|
|
|
149
193
|
Examples:
|
|
150
194
|
tl brands similar Nike
|
|
@@ -93,7 +93,7 @@ def _handle_channel_api_error(e: ApiError) -> None:
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def _format_score(results: list[dict]) -> list[dict]:
|
|
96
|
-
"""Convert raw
|
|
96
|
+
"""Convert raw similarity score (0.0-1.0) to percentage string for table/csv/md."""
|
|
97
97
|
for row in results:
|
|
98
98
|
score = row.get("score")
|
|
99
99
|
if isinstance(score, (int, float)):
|
|
@@ -171,14 +171,14 @@ def similar_cmd(
|
|
|
171
171
|
) -> None:
|
|
172
172
|
"""Find channels similar to a given one (by id or name).
|
|
173
173
|
|
|
174
|
-
Costs
|
|
175
|
-
ranked by
|
|
174
|
+
Costs 25 credits per call. Intelligence plan required. Results are
|
|
175
|
+
ranked by similarity and enriched with subscribers, impression,
|
|
176
176
|
total_views, category, and the channel's representative CPM.
|
|
177
177
|
|
|
178
178
|
Server-side filters (pushed to the recommender):
|
|
179
179
|
language:<iso> Restrict to a content language (default: en)
|
|
180
180
|
msn:<true|false> Restrict to Media Selling Network (default: true)
|
|
181
|
-
min-score:<0-1> Minimum
|
|
181
|
+
min-score:<0-1> Minimum similarity (default: 0.5)
|
|
182
182
|
|
|
183
183
|
Client-side post-filters (applied after fetch):
|
|
184
184
|
category:<code> Keep only rows matching this content_category
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""tl recommender —
|
|
1
|
+
"""tl recommender — Recommender introspection and discovery.
|
|
2
2
|
|
|
3
|
-
Surfaces the channel/profile
|
|
4
|
-
"Recommender Insights" web view: list the
|
|
5
|
-
demographics, formats, etc.), find the top channels and profiles
|
|
6
|
-
on a given tag, inspect a single channel or brand
|
|
7
|
-
channels similar to a brand
|
|
3
|
+
Surfaces the channel/profile similarity machinery that powers the
|
|
4
|
+
"Recommender Insights" web view: list the similarity tags (categories,
|
|
5
|
+
demographics, formats, etc.), find the top channels and profiles
|
|
6
|
+
scoring high on a given tag, inspect a single channel or brand
|
|
7
|
+
similarity profile, fetch channels similar to a brand's ideal profile,
|
|
8
|
+
or fetch brands likely to sponsor a given channel.
|
|
8
9
|
|
|
9
10
|
For 1:1 similarity use `tl channels similar` and `tl brands similar`.
|
|
10
11
|
"""
|
|
@@ -19,7 +20,7 @@ from tl_cli.client.http import get_client
|
|
|
19
20
|
from tl_cli.filters import parse_filters
|
|
20
21
|
from tl_cli.output.formatter import detect_format, output, output_single
|
|
21
22
|
|
|
22
|
-
app = typer.Typer(help="
|
|
23
|
+
app = typer.Typer(help="Recommender (similarity tags, top-channels/profiles/brands, similarity-profile inspection, profile→channel and channel→brand similarity)")
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
TOP_CHANNEL_COLUMNS = ["value", "channel_id", "channel_name", "slug"]
|
|
@@ -46,7 +47,7 @@ def _handle_recommender_error(e: ApiError) -> None:
|
|
|
46
47
|
|
|
47
48
|
@app.callback(invoke_without_command=True)
|
|
48
49
|
def recommender(ctx: typer.Context) -> None:
|
|
49
|
-
"""
|
|
50
|
+
"""Recommender."""
|
|
50
51
|
if ctx.invoked_subcommand is None:
|
|
51
52
|
typer.echo(ctx.get_help())
|
|
52
53
|
|
|
@@ -59,10 +60,10 @@ def tags_cmd(
|
|
|
59
60
|
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
60
61
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
61
62
|
) -> None:
|
|
62
|
-
"""List
|
|
63
|
+
"""List similarity tag names (free).
|
|
63
64
|
|
|
64
65
|
Use this to discover the tag names accepted by `tl recommender top`.
|
|
65
|
-
Each tag is one
|
|
66
|
+
Each tag is one signal in a channel or brand similarity profile —
|
|
66
67
|
e.g. content categories like "Cooking", demographic buckets like
|
|
67
68
|
"Age 18-24", device shares, country shares.
|
|
68
69
|
|
|
@@ -81,7 +82,7 @@ def tags_cmd(
|
|
|
81
82
|
data,
|
|
82
83
|
fmt,
|
|
83
84
|
columns=["group", "field_name"],
|
|
84
|
-
title="
|
|
85
|
+
title="Similarity tags",
|
|
85
86
|
)
|
|
86
87
|
except ApiError as e:
|
|
87
88
|
handle_api_error(e)
|
|
@@ -127,7 +128,7 @@ def _do_top(kind: str, tag: str, args: list[str], fmt: str, limit: int, columns:
|
|
|
127
128
|
|
|
128
129
|
@app.command("top-channels")
|
|
129
130
|
def top_channels_cmd(
|
|
130
|
-
tag: str = typer.Argument(..., help='
|
|
131
|
+
tag: str = typer.Argument(..., help='Similarity tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
|
|
131
132
|
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
132
133
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
133
134
|
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
@@ -135,9 +136,9 @@ def top_channels_cmd(
|
|
|
135
136
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
136
137
|
limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
|
|
137
138
|
) -> None:
|
|
138
|
-
"""Top channels
|
|
139
|
+
"""Top channels scoring high on a single similarity tag.
|
|
139
140
|
|
|
140
|
-
Costs
|
|
141
|
+
Costs 25 credits per call. Intelligence plan required.
|
|
141
142
|
|
|
142
143
|
Filters:
|
|
143
144
|
msn:<yes|no|all> MSN membership (default: all)
|
|
@@ -154,7 +155,7 @@ def top_channels_cmd(
|
|
|
154
155
|
|
|
155
156
|
@app.command("top-profiles")
|
|
156
157
|
def top_profiles_cmd(
|
|
157
|
-
tag: str = typer.Argument(..., help='
|
|
158
|
+
tag: str = typer.Argument(..., help='Similarity tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
|
|
158
159
|
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
159
160
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
160
161
|
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
@@ -162,9 +163,9 @@ def top_profiles_cmd(
|
|
|
162
163
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
163
164
|
limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
|
|
164
165
|
) -> None:
|
|
165
|
-
"""Top brand profiles
|
|
166
|
+
"""Top brand profiles scoring high on a single similarity tag.
|
|
166
167
|
|
|
167
|
-
Costs
|
|
168
|
+
Costs 25 credits per call. Intelligence plan required. Profiles can
|
|
168
169
|
represent the same brand more than once (one brand → multiple
|
|
169
170
|
profiles); use `top-brands` for brand-deduplicated results.
|
|
170
171
|
|
|
@@ -182,7 +183,7 @@ def top_profiles_cmd(
|
|
|
182
183
|
|
|
183
184
|
@app.command("top-brands")
|
|
184
185
|
def top_brands_cmd(
|
|
185
|
-
tag: str = typer.Argument(..., help='
|
|
186
|
+
tag: str = typer.Argument(..., help='Similarity tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
|
|
186
187
|
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
187
188
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
188
189
|
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
@@ -190,9 +191,9 @@ def top_brands_cmd(
|
|
|
190
191
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
191
192
|
limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
|
|
192
193
|
) -> None:
|
|
193
|
-
"""Top brands
|
|
194
|
+
"""Top brands scoring high on a single similarity tag (deduplicated from profiles).
|
|
194
195
|
|
|
195
|
-
Costs
|
|
196
|
+
Costs 25 credits per call. Intelligence plan required. Server-side
|
|
196
197
|
aggregates the underlying profile rows by brand, keeping the
|
|
197
198
|
highest-scoring profile per brand.
|
|
198
199
|
|
|
@@ -216,10 +217,10 @@ def inspect_channel_cmd(
|
|
|
216
217
|
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
217
218
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
218
219
|
) -> None:
|
|
219
|
-
"""Show a channel's
|
|
220
|
+
"""Show a channel's similarity profile grouped by category.
|
|
220
221
|
|
|
221
|
-
Costs
|
|
222
|
-
|
|
222
|
+
Costs 25 credits per call. Intelligence plan required. Returns the
|
|
223
|
+
active similarity tags grouped by category, plus the overall strength.
|
|
223
224
|
|
|
224
225
|
Examples:
|
|
225
226
|
tl recommender inspect-channel 12345
|
|
@@ -245,11 +246,11 @@ def inspect_brand_cmd(
|
|
|
245
246
|
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
246
247
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
247
248
|
) -> None:
|
|
248
|
-
"""Show a brand profile's ideal
|
|
249
|
+
"""Show a brand profile's ideal similarity profile grouped by category.
|
|
249
250
|
|
|
250
|
-
Costs
|
|
251
|
+
Costs 25 credits per call. Intelligence plan required. Resolves the
|
|
251
252
|
brand to its (preferred MBN) profile and inspects that profile's
|
|
252
|
-
aggregated
|
|
253
|
+
aggregated similarity tags.
|
|
253
254
|
|
|
254
255
|
Examples:
|
|
255
256
|
tl recommender inspect-brand 287
|
|
@@ -277,9 +278,9 @@ def similar_to_profile_cmd(
|
|
|
277
278
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
278
279
|
limit: int = typer.Option(20, "--limit", "-l", help="Max results (1-100)"),
|
|
279
280
|
) -> None:
|
|
280
|
-
"""Channels closest to a brand profile's ideal
|
|
281
|
+
"""Channels closest to a brand profile's ideal similarity profile.
|
|
281
282
|
|
|
282
|
-
Costs
|
|
283
|
+
Costs 25 credits per call. Intelligence plan required. Channels the
|
|
283
284
|
brand has already worked with or been proposed are excluded.
|
|
284
285
|
|
|
285
286
|
Filters:
|
|
@@ -312,3 +313,51 @@ def similar_to_profile_cmd(
|
|
|
312
313
|
handle_api_error(e)
|
|
313
314
|
finally:
|
|
314
315
|
client.close()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@app.command("similar-brands-to-channel")
|
|
319
|
+
def similar_brands_to_channel_cmd(
|
|
320
|
+
channel_ref: str = typer.Argument(..., help="Channel ID (numeric) or name (partial match, must be unique)"),
|
|
321
|
+
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
322
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
323
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
324
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
325
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
326
|
+
limit: int = typer.Option(20, "--limit", "-l", help="Max results (1-100)"),
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Brands most likely to sponsor a given channel.
|
|
329
|
+
|
|
330
|
+
Compares the channel's similarity profile against brand similarity
|
|
331
|
+
profiles and dedupes the results by brand. Costs 25 credits per call.
|
|
332
|
+
Intelligence plan required.
|
|
333
|
+
|
|
334
|
+
Filters:
|
|
335
|
+
mbn:<yes|no|all> MBN membership of the underlying profile (default: all)
|
|
336
|
+
|
|
337
|
+
Examples:
|
|
338
|
+
tl recommender similar-brands-to-channel 12345
|
|
339
|
+
tl recommender similar-brands-to-channel "MrBeast" mbn:yes --limit 30
|
|
340
|
+
"""
|
|
341
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
342
|
+
filters = parse_filters(args or [])
|
|
343
|
+
params = {k: v for k, v in filters.items() if k in {"mbn"}}
|
|
344
|
+
params["limit"] = str(limit)
|
|
345
|
+
encoded = urllib.parse.quote(channel_ref, safe="")
|
|
346
|
+
client = get_client()
|
|
347
|
+
try:
|
|
348
|
+
data = client.get(f"/recommender/channels/{encoded}/similar-brands", params=params)
|
|
349
|
+
for r in data.get("results", []):
|
|
350
|
+
score = r.get("score")
|
|
351
|
+
if isinstance(score, (int, float)) and fmt in ("table", "md"):
|
|
352
|
+
r["score"] = f"{score * 100:.1f}%"
|
|
353
|
+
output(
|
|
354
|
+
data,
|
|
355
|
+
fmt,
|
|
356
|
+
columns=["score", "brand_id", "brand_name", "website", "mbn", "profile_id"],
|
|
357
|
+
title=f"Brands likely to sponsor channel {channel_ref}",
|
|
358
|
+
column_config={"score": {"justify": "right"}},
|
|
359
|
+
)
|
|
360
|
+
except ApiError as e:
|
|
361
|
+
_handle_recommender_error(e)
|
|
362
|
+
finally:
|
|
363
|
+
client.close()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""tl schema — Show raw-db schema documentation for `tl db pg|fb|es`."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.markdown import Markdown
|
|
8
|
+
|
|
9
|
+
from tl_cli.client.errors import ApiError, handle_api_error
|
|
10
|
+
from tl_cli.client.http import get_client
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Show schema documentation for raw db queries (`tl db pg|fb|es`)")
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _show(db: str, json_output: bool, table: str | None = None) -> None:
|
|
17
|
+
client = get_client()
|
|
18
|
+
try:
|
|
19
|
+
params = {"table": table} if table else {}
|
|
20
|
+
data = client.get(f"/raw/{db}/schema", params=params)
|
|
21
|
+
if json_output:
|
|
22
|
+
print(json.dumps(data, indent=2, default=str))
|
|
23
|
+
return
|
|
24
|
+
content = data.get("content", "")
|
|
25
|
+
if console.is_terminal:
|
|
26
|
+
console.print(Markdown(content))
|
|
27
|
+
else:
|
|
28
|
+
print(content)
|
|
29
|
+
except ApiError as e:
|
|
30
|
+
handle_api_error(e)
|
|
31
|
+
finally:
|
|
32
|
+
client.close()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command("pg")
|
|
36
|
+
def pg_cmd(
|
|
37
|
+
table: str = typer.Argument(None, help="Optional table name. When given, prints only that table's section in the same markdown format."),
|
|
38
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Show PostgreSQL schema reference (for `tl db pg`).
|
|
41
|
+
|
|
42
|
+
With no argument: lists every table visible to your role.
|
|
43
|
+
With a table name: prints only that table's column listing.
|
|
44
|
+
|
|
45
|
+
**Strongly preferred for single-table lookups.** Listing every
|
|
46
|
+
table just to read one is wasteful — pass the table name and the
|
|
47
|
+
server returns only that section.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
tl schema pg
|
|
51
|
+
tl schema pg thoughtleaders_channel
|
|
52
|
+
tl schema pg thoughtleaders_adlink --json
|
|
53
|
+
"""
|
|
54
|
+
_show("pg", json_output, table=table)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command("fb")
|
|
58
|
+
def fb_cmd(
|
|
59
|
+
table: str = typer.Argument(None, help="Optional table name (`article_metrics` or `channel_metrics`). When given, prints only that table's section."),
|
|
60
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Show Firebolt schema (live: tables and column types) for `tl db fb`.
|
|
63
|
+
|
|
64
|
+
With no argument: lists both accepted tables.
|
|
65
|
+
With a table name: prints only that table's columns + primary index.
|
|
66
|
+
|
|
67
|
+
**Strongly preferred for single-table lookups.** Pass the table
|
|
68
|
+
name to skip the other one.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
tl schema fb
|
|
72
|
+
tl schema fb article_metrics
|
|
73
|
+
tl schema fb channel_metrics --json
|
|
74
|
+
"""
|
|
75
|
+
_show("fb", json_output, table=table)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command("es")
|
|
79
|
+
def es_cmd(
|
|
80
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Show Elasticsearch document shape for `tl db es`."""
|
|
83
|
+
_show("es", json_output)
|
|
@@ -3,16 +3,11 @@
|
|
|
3
3
|
Query sponsorship data, channels, brands, and intelligence.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import re
|
|
7
6
|
import sys
|
|
8
7
|
import traceback
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Optional
|
|
11
8
|
|
|
12
|
-
import click
|
|
13
9
|
import typer
|
|
14
10
|
from rich.console import Console
|
|
15
|
-
from rich.markdown import Markdown
|
|
16
11
|
|
|
17
12
|
from tl_cli import __version__
|
|
18
13
|
from tl_cli import config as tl_config
|
|
@@ -128,61 +123,6 @@ def update_command() -> None:
|
|
|
128
123
|
raise typer.Exit()
|
|
129
124
|
|
|
130
125
|
|
|
131
|
-
def _get_terminology() -> str | None:
|
|
132
|
-
"""Extract the Terminology section from README.md.
|
|
133
|
-
|
|
134
|
-
Tries to locate README.md relative to the package source first,
|
|
135
|
-
then falls back to importlib.metadata.
|
|
136
|
-
"""
|
|
137
|
-
try:
|
|
138
|
-
text = None
|
|
139
|
-
readme = Path(__file__).resolve().parent.parent.parent / "README.md"
|
|
140
|
-
if readme.is_file():
|
|
141
|
-
text = readme.read_text()
|
|
142
|
-
else:
|
|
143
|
-
from importlib.metadata import metadata
|
|
144
|
-
text = metadata("thoughtleaders-cli").get_payload()
|
|
145
|
-
if not text:
|
|
146
|
-
return None
|
|
147
|
-
match = re.search(r"^# Terminology\s*\n(.+?)(?=\n# |\Z)", text, re.DOTALL | re.MULTILINE)
|
|
148
|
-
if not match:
|
|
149
|
-
return None
|
|
150
|
-
return match.group(1).strip()
|
|
151
|
-
except Exception:
|
|
152
|
-
return None
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@app.command(name="help", hidden=True)
|
|
156
|
-
def help_command(
|
|
157
|
-
ctx: typer.Context,
|
|
158
|
-
command: Optional[str] = typer.Argument(None, help="Command to show help for"),
|
|
159
|
-
) -> None:
|
|
160
|
-
"""Show help for the CLI or a specific command."""
|
|
161
|
-
root_ctx = ctx.parent
|
|
162
|
-
root_cmd = root_ctx.command
|
|
163
|
-
|
|
164
|
-
if command is None:
|
|
165
|
-
click.echo(root_cmd.get_help(root_ctx))
|
|
166
|
-
terminology = _get_terminology()
|
|
167
|
-
if terminology:
|
|
168
|
-
import shutil
|
|
169
|
-
term_width = shutil.get_terminal_size().columns
|
|
170
|
-
console = Console(width=int(term_width * 0.9))
|
|
171
|
-
console.print(Markdown(terminology))
|
|
172
|
-
console.print()
|
|
173
|
-
raise typer.Exit()
|
|
174
|
-
|
|
175
|
-
# Look up the subcommand
|
|
176
|
-
sub_cmd = root_cmd.get_command(root_ctx, command)
|
|
177
|
-
if sub_cmd is None:
|
|
178
|
-
click.echo(f"Unknown command: {command}", err=True)
|
|
179
|
-
raise typer.Exit(1)
|
|
180
|
-
|
|
181
|
-
sub_ctx = click.Context(sub_cmd, info_name=command, parent=root_ctx)
|
|
182
|
-
click.echo(sub_cmd.get_help(sub_ctx))
|
|
183
|
-
raise typer.Exit()
|
|
184
|
-
|
|
185
|
-
|
|
186
126
|
def cli() -> None:
|
|
187
127
|
"""Entry point that wraps the Typer app with top-level error handling.
|
|
188
128
|
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"""tl schema — Show raw-db schema documentation for `tl db pg|fb|es`."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
import typer
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
from rich.markdown import Markdown
|
|
8
|
-
|
|
9
|
-
from tl_cli.client.errors import ApiError, handle_api_error
|
|
10
|
-
from tl_cli.client.http import get_client
|
|
11
|
-
|
|
12
|
-
app = typer.Typer(help="Show schema documentation for raw db queries (`tl db pg|fb|es`)")
|
|
13
|
-
console = Console()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _show(db: str, json_output: bool) -> None:
|
|
17
|
-
client = get_client()
|
|
18
|
-
try:
|
|
19
|
-
data = client.get(f"/raw/{db}/schema")
|
|
20
|
-
if json_output:
|
|
21
|
-
print(json.dumps(data, indent=2, default=str))
|
|
22
|
-
return
|
|
23
|
-
content = data.get("content", "")
|
|
24
|
-
if console.is_terminal:
|
|
25
|
-
console.print(Markdown(content))
|
|
26
|
-
else:
|
|
27
|
-
print(content)
|
|
28
|
-
except ApiError as e:
|
|
29
|
-
handle_api_error(e)
|
|
30
|
-
finally:
|
|
31
|
-
client.close()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@app.command("pg")
|
|
35
|
-
def pg_cmd(
|
|
36
|
-
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
37
|
-
) -> None:
|
|
38
|
-
"""Show PostgreSQL schema reference (for `tl db pg`)."""
|
|
39
|
-
_show("pg", json_output)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@app.command("fb")
|
|
43
|
-
def fb_cmd(
|
|
44
|
-
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
45
|
-
) -> None:
|
|
46
|
-
"""Show Firebolt schema (live: tables and column types) for `tl db fb`."""
|
|
47
|
-
_show("fb", json_output)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@app.command("es")
|
|
51
|
-
def es_cmd(
|
|
52
|
-
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
53
|
-
) -> None:
|
|
54
|
-
"""Show Elasticsearch document shape for `tl db es`."""
|
|
55
|
-
_show("es", json_output)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/business-glossary.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|