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.
- earlyforge_cli-0.1.0/PKG-INFO +99 -0
- earlyforge_cli-0.1.0/README.md +85 -0
- earlyforge_cli-0.1.0/earlyforge_cli/__init__.py +1 -0
- earlyforge_cli-0.1.0/earlyforge_cli/client.py +85 -0
- earlyforge_cli-0.1.0/earlyforge_cli/main.py +144 -0
- earlyforge_cli-0.1.0/earlyforge_cli.egg-info/PKG-INFO +99 -0
- earlyforge_cli-0.1.0/earlyforge_cli.egg-info/SOURCES.txt +12 -0
- earlyforge_cli-0.1.0/earlyforge_cli.egg-info/dependency_links.txt +1 -0
- earlyforge_cli-0.1.0/earlyforge_cli.egg-info/entry_points.txt +2 -0
- earlyforge_cli-0.1.0/earlyforge_cli.egg-info/requires.txt +2 -0
- earlyforge_cli-0.1.0/earlyforge_cli.egg-info/top_level.txt +1 -0
- earlyforge_cli-0.1.0/pyproject.toml +24 -0
- earlyforge_cli-0.1.0/setup.cfg +4 -0
- earlyforge_cli-0.1.0/tests/test_cli.py +51 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|