earlyforge-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: earlyforge-cli
3
+ Version: 0.1.0
4
+ Summary: Command-line client for the EarlyForge Pulse API (free trend data, authenticated)
5
+ Author: EarlyForge
6
+ License: MIT
7
+ Project-URL: Homepage, https://earlyforge.ai
8
+ Project-URL: Documentation, https://docs.earlyforge.ai/guides/api/trends-api
9
+ Project-URL: Source, https://github.com/Earlyforge-official/earlyforge
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: click>=8.1.0
13
+ Requires-Dist: httpx>=0.27.0
14
+
15
+ # earlyforge-cli
16
+
17
+ Command-line client for the **EarlyForge Trends API**, with two tiers
18
+ under the same `/v1/api/trends/*` prefix:
19
+
20
+ - **Pulse mirror** (`pulse top|region|category|trend`) — free on every
21
+ account, no plan required. The same daily snapshot already shown on
22
+ [pulse.earlyforge.ai](https://pulse.earlyforge.ai).
23
+ - **Full trends feed** (`pulse full`/`pulse full-trend`) — paid plans
24
+ only, and **not "all trends unlocked."** Every plan reveals a bigger,
25
+ but still limited, daily, random, category-diversified budget of trend
26
+ names — exactly what your organization's dashboard Trends page shows
27
+ unlocked today, no more, no less.
28
+
29
+ Not a CLI for articles, briefs, or alerts — the public API never exposes
30
+ those on any plan. Full reference: [docs.earlyforge.ai/guides/api/cli](https://docs.earlyforge.ai/guides/api/cli). Source: [github.com/Earlyforge-official/earlyforge](https://github.com/Earlyforge-official/earlyforge/tree/main/cli-earlyforge).
31
+
32
+ ## Install
33
+
34
+ Not published yet. Contact [support@earlyforge.ai](mailto:support@earlyforge.ai) for early access.
35
+
36
+ ## Configure
37
+
38
+ ```bash
39
+ export EARLYFORGE_API_KEY=ef_live_xxxxxxxxxxxxxxxx
40
+ ```
41
+
42
+ Get a key from **Settings → API Keys** in the EarlyForge console. Or pass
43
+ `--api-key` on each command instead of the env var.
44
+
45
+ ## Commands
46
+
47
+ ```bash
48
+ earlyforge pulse top [--date YYYY-MM-DD] [--locale fr]
49
+ earlyforge pulse region <REGION> [--date YYYY-MM-DD] [--locale fr]
50
+ earlyforge pulse category <CATEGORY> [--date YYYY-MM-DD] [--locale fr]
51
+ earlyforge pulse trend <TREND_ID>
52
+ earlyforge pulse full [--region US] [--category tech] [--page 1] [--limit 20]
53
+ earlyforge pulse full-trend <TREND_ID>
54
+ ```
55
+
56
+ Every command pretty-prints the JSON response and accepts `--api-key` and
57
+ `--base-url` (default `https://api.earlyforge.ai`, useful for pointing at
58
+ a local gateway in development).
59
+
60
+ `pulse full` and `pulse full-trend` require a paid-plan API key — a
61
+ free-plan key gets HTTP 402. They are **not** "all trends unlocked":
62
+ every plan, paid included, only reveals a daily, random,
63
+ category-diversified budget of trend names, same as the dashboard's
64
+ Trends page. A trend outside that budget stays in the list with its name
65
+ redacted (`"name_locked": true`) for `pulse full`, or returns HTTP 402
66
+ for `pulse full-trend`.
67
+
68
+ ### Examples
69
+
70
+ ```bash
71
+ earlyforge pulse top
72
+ earlyforge pulse top --locale fr --date 2026-11-01
73
+ earlyforge pulse region US
74
+ earlyforge pulse category politics
75
+ earlyforge pulse trend tr_a1b2c3
76
+ earlyforge pulse full --region US --limit 50
77
+ earlyforge pulse full-trend tr_a1b2c3
78
+ ```
79
+
80
+ ## Errors
81
+
82
+ A non-2xx response prints `Error: ... (<detail>)` to stderr and exits
83
+ with status `1` — the same `401` invalid-key / `404` unknown-trend /
84
+ `502` upstream-unavailable cases documented in the OpenAPI spec, plus
85
+ `402` (`upgrade_required` on `pulse full`/`pulse full-trend` for a
86
+ free-plan key, `trend_locked` on `pulse full-trend` for a trend outside
87
+ today's budget).
88
+
89
+ ## Rate limits
90
+
91
+ No monthly call quota on this surface — only the shared 60 requests/60s
92
+ burst limit applies, same as the anonymous Pulse page.
93
+
94
+ ## Tests
95
+
96
+ ```bash
97
+ pip install -e . pytest
98
+ pytest
99
+ ```
@@ -0,0 +1,85 @@
1
+ # earlyforge-cli
2
+
3
+ Command-line client for the **EarlyForge Trends API**, with two tiers
4
+ under the same `/v1/api/trends/*` prefix:
5
+
6
+ - **Pulse mirror** (`pulse top|region|category|trend`) — free on every
7
+ account, no plan required. The same daily snapshot already shown on
8
+ [pulse.earlyforge.ai](https://pulse.earlyforge.ai).
9
+ - **Full trends feed** (`pulse full`/`pulse full-trend`) — paid plans
10
+ only, and **not "all trends unlocked."** Every plan reveals a bigger,
11
+ but still limited, daily, random, category-diversified budget of trend
12
+ names — exactly what your organization's dashboard Trends page shows
13
+ unlocked today, no more, no less.
14
+
15
+ Not a CLI for articles, briefs, or alerts — the public API never exposes
16
+ those on any plan. Full reference: [docs.earlyforge.ai/guides/api/cli](https://docs.earlyforge.ai/guides/api/cli). Source: [github.com/Earlyforge-official/earlyforge](https://github.com/Earlyforge-official/earlyforge/tree/main/cli-earlyforge).
17
+
18
+ ## Install
19
+
20
+ Not published yet. Contact [support@earlyforge.ai](mailto:support@earlyforge.ai) for early access.
21
+
22
+ ## Configure
23
+
24
+ ```bash
25
+ export EARLYFORGE_API_KEY=ef_live_xxxxxxxxxxxxxxxx
26
+ ```
27
+
28
+ Get a key from **Settings → API Keys** in the EarlyForge console. Or pass
29
+ `--api-key` on each command instead of the env var.
30
+
31
+ ## Commands
32
+
33
+ ```bash
34
+ earlyforge pulse top [--date YYYY-MM-DD] [--locale fr]
35
+ earlyforge pulse region <REGION> [--date YYYY-MM-DD] [--locale fr]
36
+ earlyforge pulse category <CATEGORY> [--date YYYY-MM-DD] [--locale fr]
37
+ earlyforge pulse trend <TREND_ID>
38
+ earlyforge pulse full [--region US] [--category tech] [--page 1] [--limit 20]
39
+ earlyforge pulse full-trend <TREND_ID>
40
+ ```
41
+
42
+ Every command pretty-prints the JSON response and accepts `--api-key` and
43
+ `--base-url` (default `https://api.earlyforge.ai`, useful for pointing at
44
+ a local gateway in development).
45
+
46
+ `pulse full` and `pulse full-trend` require a paid-plan API key — a
47
+ free-plan key gets HTTP 402. They are **not** "all trends unlocked":
48
+ every plan, paid included, only reveals a daily, random,
49
+ category-diversified budget of trend names, same as the dashboard's
50
+ Trends page. A trend outside that budget stays in the list with its name
51
+ redacted (`"name_locked": true`) for `pulse full`, or returns HTTP 402
52
+ for `pulse full-trend`.
53
+
54
+ ### Examples
55
+
56
+ ```bash
57
+ earlyforge pulse top
58
+ earlyforge pulse top --locale fr --date 2026-11-01
59
+ earlyforge pulse region US
60
+ earlyforge pulse category politics
61
+ earlyforge pulse trend tr_a1b2c3
62
+ earlyforge pulse full --region US --limit 50
63
+ earlyforge pulse full-trend tr_a1b2c3
64
+ ```
65
+
66
+ ## Errors
67
+
68
+ A non-2xx response prints `Error: ... (<detail>)` to stderr and exits
69
+ with status `1` — the same `401` invalid-key / `404` unknown-trend /
70
+ `502` upstream-unavailable cases documented in the OpenAPI spec, plus
71
+ `402` (`upgrade_required` on `pulse full`/`pulse full-trend` for a
72
+ free-plan key, `trend_locked` on `pulse full-trend` for a trend outside
73
+ today's budget).
74
+
75
+ ## Rate limits
76
+
77
+ No monthly call quota on this surface — only the shared 60 requests/60s
78
+ burst limit applies, same as the anonymous Pulse page.
79
+
80
+ ## Tests
81
+
82
+ ```bash
83
+ pip install -e . pytest
84
+ pytest
85
+ ```
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,85 @@
1
+ """Minimal, standalone Trends API client used by the CLI.
2
+
3
+ Deliberately self-contained (duplicates the method shape of
4
+ tools/sdk-python-earlyforge/earlyforge/client.py) so this package has no
5
+ dependency on another, as-yet-unpublished package. Contract:
6
+ docs/api/public-api-v1/openapi.yaml (repo root).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Dict, Optional
12
+ from urllib.parse import quote
13
+
14
+ import httpx
15
+
16
+ DEFAULT_BASE_URL = "https://api.earlyforge.ai"
17
+ DEFAULT_TIMEOUT_SECONDS = 15.0
18
+
19
+
20
+ class PulseApiError(Exception):
21
+ def __init__(self, message: str, *, status_code: Optional[int] = None, detail: Optional[str] = None) -> None:
22
+ super().__init__(message)
23
+ self.status_code = status_code
24
+ self.detail = detail
25
+
26
+
27
+ class PulseClient:
28
+ def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL) -> None:
29
+ if not api_key:
30
+ raise ValueError("api_key is required")
31
+ self.api_key = api_key
32
+ self.base_url = base_url.rstrip("/")
33
+
34
+ def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
35
+ clean_params = {k: v for k, v in (params or {}).items() if v is not None}
36
+ url = f"{self.base_url}{path}"
37
+ try:
38
+ response = httpx.get(
39
+ url,
40
+ params=clean_params,
41
+ headers={"Authorization": f"Bearer {self.api_key}"},
42
+ timeout=DEFAULT_TIMEOUT_SECONDS,
43
+ )
44
+ except httpx.RequestError as exc:
45
+ raise PulseApiError(f"Could not reach EarlyForge API at {url}: {exc}") from exc
46
+
47
+ if response.status_code >= 400:
48
+ try:
49
+ detail = response.json().get("detail")
50
+ except Exception:
51
+ detail = response.text[:300] or None
52
+ raise PulseApiError(
53
+ f"EarlyForge API returned HTTP {response.status_code} for {url}",
54
+ status_code=response.status_code,
55
+ detail=detail,
56
+ )
57
+ return response.json()
58
+
59
+ def top_trends(self, *, date: Optional[str] = None, locale: Optional[str] = None) -> Any:
60
+ return self._get("/v1/api/trends/top", {"date": date, "locale": locale})
61
+
62
+ def trends_by_region(self, region: str, *, date: Optional[str] = None, locale: Optional[str] = None) -> Any:
63
+ return self._get(f"/v1/api/trends/region/{quote(region, safe='')}", {"date": date, "locale": locale})
64
+
65
+ def trends_by_category(self, category: str, *, date: Optional[str] = None, locale: Optional[str] = None) -> Any:
66
+ return self._get(f"/v1/api/trends/category/{quote(category, safe='')}", {"date": date, "locale": locale})
67
+
68
+ def get_trend(self, trend_id: str) -> Any:
69
+ return self._get(f"/v1/api/trends/{quote(trend_id, safe='')}")
70
+
71
+ def full_trends(
72
+ self,
73
+ *,
74
+ region: Optional[str] = None,
75
+ category: Optional[str] = None,
76
+ page: int = 1,
77
+ limit: int = 20,
78
+ ) -> Any:
79
+ return self._get(
80
+ "/v1/api/trends/full",
81
+ {"region": region, "category": category, "page": page, "limit": limit},
82
+ )
83
+
84
+ def get_full_trend(self, trend_id: str) -> Any:
85
+ return self._get(f"/v1/api/trends/full/{quote(trend_id, safe='')}")
@@ -0,0 +1,144 @@
1
+ """`earlyforge` CLI — command-line access to the EarlyForge Trends API.
2
+
3
+ Wraps the same endpoints as the Python/JS/PHP SDKs and the MCP server:
4
+ the 4 free Pulse endpoints (top, by region, by category, trend detail)
5
+ plus the paid-plan full trends feed (list + detail). Contract:
6
+ docs/api/public-api-v1/openapi.yaml (repo root).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+ from typing import Optional
15
+
16
+ import click
17
+
18
+ from .client import PulseApiError, PulseClient
19
+
20
+
21
+ def _resolve_api_key(api_key: Optional[str]) -> str:
22
+ resolved = api_key or os.environ.get("EARLYFORGE_API_KEY")
23
+ if not resolved:
24
+ raise click.ClickException(
25
+ "No API key provided. Pass --api-key or set EARLYFORGE_API_KEY. "
26
+ "Generate one from Settings -> API Keys in the EarlyForge console "
27
+ "(any account — Pulse access is free)."
28
+ )
29
+ return resolved
30
+
31
+
32
+ def _print_json(payload: object) -> None:
33
+ click.echo(json.dumps(payload, indent=2, ensure_ascii=False))
34
+
35
+
36
+ @click.group()
37
+ def cli() -> None:
38
+ """EarlyForge command-line client."""
39
+
40
+
41
+ @cli.group()
42
+ def pulse() -> None:
43
+ """Query the free EarlyForge Pulse API (trend snapshots)."""
44
+
45
+
46
+ @pulse.command("top")
47
+ @click.option("--date", default=None, help="Past daily snapshot (YYYY-MM-DD). Omit for latest.")
48
+ @click.option("--locale", default=None, help='Locale for translated fields (e.g. "fr").')
49
+ @click.option("--api-key", default=None, help="API key. Defaults to $EARLYFORGE_API_KEY.")
50
+ @click.option("--base-url", default="https://api.earlyforge.ai", show_default=True)
51
+ def pulse_top(date: Optional[str], locale: Optional[str], api_key: Optional[str], base_url: str) -> None:
52
+ """Global top trends — same snapshot as the Pulse homepage."""
53
+ client = PulseClient(api_key=_resolve_api_key(api_key), base_url=base_url)
54
+ _run(lambda: client.top_trends(date=date, locale=locale))
55
+
56
+
57
+ @pulse.command("region")
58
+ @click.argument("region")
59
+ @click.option("--date", default=None, help="Past daily snapshot (YYYY-MM-DD). Omit for latest.")
60
+ @click.option("--locale", default=None, help='Locale for translated fields (e.g. "fr").')
61
+ @click.option("--api-key", default=None, help="API key. Defaults to $EARLYFORGE_API_KEY.")
62
+ @click.option("--base-url", default="https://api.earlyforge.ai", show_default=True)
63
+ def pulse_region(
64
+ region: str, date: Optional[str], locale: Optional[str], api_key: Optional[str], base_url: str
65
+ ) -> None:
66
+ """Top trends for a region (ISO 3166-1 alpha-2, e.g. US, FR)."""
67
+ client = PulseClient(api_key=_resolve_api_key(api_key), base_url=base_url)
68
+ _run(lambda: client.trends_by_region(region, date=date, locale=locale))
69
+
70
+
71
+ @pulse.command("category")
72
+ @click.argument("category")
73
+ @click.option("--date", default=None, help="Past daily snapshot (YYYY-MM-DD). Omit for latest.")
74
+ @click.option("--locale", default=None, help='Locale for translated fields (e.g. "fr").')
75
+ @click.option("--api-key", default=None, help="API key. Defaults to $EARLYFORGE_API_KEY.")
76
+ @click.option("--base-url", default="https://api.earlyforge.ai", show_default=True)
77
+ def pulse_category(
78
+ category: str, date: Optional[str], locale: Optional[str], api_key: Optional[str], base_url: str
79
+ ) -> None:
80
+ """Top trends for a category (e.g. politics, tech)."""
81
+ client = PulseClient(api_key=_resolve_api_key(api_key), base_url=base_url)
82
+ _run(lambda: client.trends_by_category(category, date=date, locale=locale))
83
+
84
+
85
+ @pulse.command("trend")
86
+ @click.argument("trend_id")
87
+ @click.option("--api-key", default=None, help="API key. Defaults to $EARLYFORGE_API_KEY.")
88
+ @click.option("--base-url", default="https://api.earlyforge.ai", show_default=True)
89
+ def pulse_trend(trend_id: str, api_key: Optional[str], base_url: str) -> None:
90
+ """Detail for one trend (score, description, sources, timeline)."""
91
+ client = PulseClient(api_key=_resolve_api_key(api_key), base_url=base_url)
92
+ _run(lambda: client.get_trend(trend_id))
93
+
94
+
95
+ @pulse.command("full")
96
+ @click.option("--region", default=None, help="Filter by region.")
97
+ @click.option("--category", default=None, help="Filter by category.")
98
+ @click.option("--page", default=1, show_default=True, type=int)
99
+ @click.option("--limit", default=20, show_default=True, type=int, help="Max 100.")
100
+ @click.option("--api-key", default=None, help="API key. Defaults to $EARLYFORGE_API_KEY.")
101
+ @click.option("--base-url", default="https://api.earlyforge.ai", show_default=True)
102
+ def pulse_full(
103
+ region: Optional[str],
104
+ category: Optional[str],
105
+ page: int,
106
+ limit: int,
107
+ api_key: Optional[str],
108
+ base_url: str,
109
+ ) -> None:
110
+ """Full trends feed (paid plans only — not "all trends unlocked").
111
+
112
+ Every plan still reveals only a daily, random, category-diversified
113
+ budget of trend names, same as the dashboard's Trends page. Items
114
+ outside today's budget stay in the list with their name redacted
115
+ ("name_locked": true). A free-plan key gets HTTP 402.
116
+ """
117
+ client = PulseClient(api_key=_resolve_api_key(api_key), base_url=base_url)
118
+ _run(lambda: client.full_trends(region=region, category=category, page=page, limit=limit))
119
+
120
+
121
+ @pulse.command("full-trend")
122
+ @click.argument("trend_id")
123
+ @click.option("--api-key", default=None, help="API key. Defaults to $EARLYFORGE_API_KEY.")
124
+ @click.option("--base-url", default="https://api.earlyforge.ai", show_default=True)
125
+ def pulse_full_trend(trend_id: str, api_key: Optional[str], base_url: str) -> None:
126
+ """Full detail for one trend (paid plans only).
127
+
128
+ Returns HTTP 402 if the trend is outside today's visible-name
129
+ budget — same restriction the dashboard's own detail panel enforces.
130
+ """
131
+ client = PulseClient(api_key=_resolve_api_key(api_key), base_url=base_url)
132
+ _run(lambda: client.get_full_trend(trend_id))
133
+
134
+
135
+ def _run(call) -> None:
136
+ try:
137
+ _print_json(call())
138
+ except PulseApiError as exc:
139
+ click.echo(f"Error: {exc} ({exc.detail})" if exc.detail else f"Error: {exc}", err=True)
140
+ sys.exit(1)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ cli()
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: earlyforge-cli
3
+ Version: 0.1.0
4
+ Summary: Command-line client for the EarlyForge Pulse API (free trend data, authenticated)
5
+ Author: EarlyForge
6
+ License: MIT
7
+ Project-URL: Homepage, https://earlyforge.ai
8
+ Project-URL: Documentation, https://docs.earlyforge.ai/guides/api/trends-api
9
+ Project-URL: Source, https://github.com/Earlyforge-official/earlyforge
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: click>=8.1.0
13
+ Requires-Dist: httpx>=0.27.0
14
+
15
+ # earlyforge-cli
16
+
17
+ Command-line client for the **EarlyForge Trends API**, with two tiers
18
+ under the same `/v1/api/trends/*` prefix:
19
+
20
+ - **Pulse mirror** (`pulse top|region|category|trend`) — free on every
21
+ account, no plan required. The same daily snapshot already shown on
22
+ [pulse.earlyforge.ai](https://pulse.earlyforge.ai).
23
+ - **Full trends feed** (`pulse full`/`pulse full-trend`) — paid plans
24
+ only, and **not "all trends unlocked."** Every plan reveals a bigger,
25
+ but still limited, daily, random, category-diversified budget of trend
26
+ names — exactly what your organization's dashboard Trends page shows
27
+ unlocked today, no more, no less.
28
+
29
+ Not a CLI for articles, briefs, or alerts — the public API never exposes
30
+ those on any plan. Full reference: [docs.earlyforge.ai/guides/api/cli](https://docs.earlyforge.ai/guides/api/cli). Source: [github.com/Earlyforge-official/earlyforge](https://github.com/Earlyforge-official/earlyforge/tree/main/cli-earlyforge).
31
+
32
+ ## Install
33
+
34
+ Not published yet. Contact [support@earlyforge.ai](mailto:support@earlyforge.ai) for early access.
35
+
36
+ ## Configure
37
+
38
+ ```bash
39
+ export EARLYFORGE_API_KEY=ef_live_xxxxxxxxxxxxxxxx
40
+ ```
41
+
42
+ Get a key from **Settings → API Keys** in the EarlyForge console. Or pass
43
+ `--api-key` on each command instead of the env var.
44
+
45
+ ## Commands
46
+
47
+ ```bash
48
+ earlyforge pulse top [--date YYYY-MM-DD] [--locale fr]
49
+ earlyforge pulse region <REGION> [--date YYYY-MM-DD] [--locale fr]
50
+ earlyforge pulse category <CATEGORY> [--date YYYY-MM-DD] [--locale fr]
51
+ earlyforge pulse trend <TREND_ID>
52
+ earlyforge pulse full [--region US] [--category tech] [--page 1] [--limit 20]
53
+ earlyforge pulse full-trend <TREND_ID>
54
+ ```
55
+
56
+ Every command pretty-prints the JSON response and accepts `--api-key` and
57
+ `--base-url` (default `https://api.earlyforge.ai`, useful for pointing at
58
+ a local gateway in development).
59
+
60
+ `pulse full` and `pulse full-trend` require a paid-plan API key — a
61
+ free-plan key gets HTTP 402. They are **not** "all trends unlocked":
62
+ every plan, paid included, only reveals a daily, random,
63
+ category-diversified budget of trend names, same as the dashboard's
64
+ Trends page. A trend outside that budget stays in the list with its name
65
+ redacted (`"name_locked": true`) for `pulse full`, or returns HTTP 402
66
+ for `pulse full-trend`.
67
+
68
+ ### Examples
69
+
70
+ ```bash
71
+ earlyforge pulse top
72
+ earlyforge pulse top --locale fr --date 2026-11-01
73
+ earlyforge pulse region US
74
+ earlyforge pulse category politics
75
+ earlyforge pulse trend tr_a1b2c3
76
+ earlyforge pulse full --region US --limit 50
77
+ earlyforge pulse full-trend tr_a1b2c3
78
+ ```
79
+
80
+ ## Errors
81
+
82
+ A non-2xx response prints `Error: ... (<detail>)` to stderr and exits
83
+ with status `1` — the same `401` invalid-key / `404` unknown-trend /
84
+ `502` upstream-unavailable cases documented in the OpenAPI spec, plus
85
+ `402` (`upgrade_required` on `pulse full`/`pulse full-trend` for a
86
+ free-plan key, `trend_locked` on `pulse full-trend` for a trend outside
87
+ today's budget).
88
+
89
+ ## Rate limits
90
+
91
+ No monthly call quota on this surface — only the shared 60 requests/60s
92
+ burst limit applies, same as the anonymous Pulse page.
93
+
94
+ ## Tests
95
+
96
+ ```bash
97
+ pip install -e . pytest
98
+ pytest
99
+ ```
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ earlyforge_cli/__init__.py
4
+ earlyforge_cli/client.py
5
+ earlyforge_cli/main.py
6
+ earlyforge_cli.egg-info/PKG-INFO
7
+ earlyforge_cli.egg-info/SOURCES.txt
8
+ earlyforge_cli.egg-info/dependency_links.txt
9
+ earlyforge_cli.egg-info/entry_points.txt
10
+ earlyforge_cli.egg-info/requires.txt
11
+ earlyforge_cli.egg-info/top_level.txt
12
+ tests/test_cli.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ earlyforge = earlyforge_cli.main:cli
@@ -0,0 +1,2 @@
1
+ click>=8.1.0
2
+ httpx>=0.27.0
@@ -0,0 +1 @@
1
+ earlyforge_cli
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "earlyforge-cli"
7
+ version = "0.1.0"
8
+ description = "Command-line client for the EarlyForge Pulse API (free trend data, authenticated)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "EarlyForge" }]
13
+ dependencies = ["click>=8.1.0", "httpx>=0.27.0"]
14
+
15
+ [project.scripts]
16
+ earlyforge = "earlyforge_cli.main:cli"
17
+
18
+ [project.urls]
19
+ Homepage = "https://earlyforge.ai"
20
+ Documentation = "https://docs.earlyforge.ai/guides/api/trends-api"
21
+ Source = "https://github.com/Earlyforge-official/earlyforge"
22
+
23
+ [tool.setuptools.packages.find]
24
+ include = ["earlyforge_cli*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+ import pytest
5
+ from click.testing import CliRunner
6
+
7
+ from earlyforge_cli.main import cli
8
+
9
+
10
+ def _handler(request: httpx.Request) -> httpx.Response:
11
+ assert request.headers["authorization"] == "Bearer ef_live_test"
12
+ if request.url.path == "/v1/api/trends/top":
13
+ return httpx.Response(200, json={"trends": [], "total": 0})
14
+ if request.url.path == "/v1/api/trends/full":
15
+ return httpx.Response(
16
+ 200, json={"items": [{"trend_id": "tr_1"}], "total": 1, "page": 1, "per_page": 20}
17
+ )
18
+ return httpx.Response(404, json={"detail": "Not found"})
19
+
20
+
21
+ @pytest.fixture(autouse=True)
22
+ def _patch_transport(monkeypatch: pytest.MonkeyPatch) -> None:
23
+ transport = httpx.MockTransport(_handler)
24
+
25
+ def fake_get(url, *, params=None, headers=None, timeout=None):
26
+ with httpx.Client(transport=transport) as client:
27
+ return client.get(url, params=params, headers=headers, timeout=timeout)
28
+
29
+ monkeypatch.setattr(httpx, "get", fake_get)
30
+
31
+
32
+ def test_pulse_top_prints_json() -> None:
33
+ runner = CliRunner()
34
+ result = runner.invoke(cli, ["pulse", "top", "--api-key", "ef_live_test"])
35
+ assert result.exit_code == 0
36
+ assert '"trends": []' in result.output
37
+
38
+
39
+ def test_pulse_full_prints_json() -> None:
40
+ runner = CliRunner()
41
+ result = runner.invoke(cli, ["pulse", "full", "--api-key", "ef_live_test"])
42
+ assert result.exit_code == 0
43
+ assert '"total": 1' in result.output
44
+
45
+
46
+ def test_missing_api_key_errors_cleanly(monkeypatch: pytest.MonkeyPatch) -> None:
47
+ monkeypatch.delenv("EARLYFORGE_API_KEY", raising=False)
48
+ runner = CliRunner()
49
+ result = runner.invoke(cli, ["pulse", "top"])
50
+ assert result.exit_code != 0
51
+ assert "No API key provided" in result.output