openrssgate 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.
- openrssgate-0.1.0/PKG-INFO +81 -0
- openrssgate-0.1.0/README.md +57 -0
- openrssgate-0.1.0/openrssgate/__init__.py +3 -0
- openrssgate-0.1.0/openrssgate/client.py +49 -0
- openrssgate-0.1.0/openrssgate/commands/__init__.py +1 -0
- openrssgate-0.1.0/openrssgate/commands/feeds.py +70 -0
- openrssgate-0.1.0/openrssgate/commands/list.py +50 -0
- openrssgate-0.1.0/openrssgate/commands/stats.py +21 -0
- openrssgate-0.1.0/openrssgate/commands/validate.py +29 -0
- openrssgate-0.1.0/openrssgate/config.py +7 -0
- openrssgate-0.1.0/openrssgate/main.py +34 -0
- openrssgate-0.1.0/openrssgate.egg-info/PKG-INFO +81 -0
- openrssgate-0.1.0/openrssgate.egg-info/SOURCES.txt +19 -0
- openrssgate-0.1.0/openrssgate.egg-info/dependency_links.txt +1 -0
- openrssgate-0.1.0/openrssgate.egg-info/entry_points.txt +2 -0
- openrssgate-0.1.0/openrssgate.egg-info/requires.txt +2 -0
- openrssgate-0.1.0/openrssgate.egg-info/top_level.txt +1 -0
- openrssgate-0.1.0/pyproject.toml +41 -0
- openrssgate-0.1.0/setup.cfg +4 -0
- openrssgate-0.1.0/setup.py +17 -0
- openrssgate-0.1.0/tests/test_client.py +123 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openrssgate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only CLI for querying OpenRSSGate sources and feeds.
|
|
5
|
+
Author: OpenRSSGate
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/windbug99/openrssgate
|
|
8
|
+
Project-URL: Repository, https://github.com/windbug99/openrssgate
|
|
9
|
+
Project-URL: Issues, https://github.com/windbug99/openrssgate/issues
|
|
10
|
+
Keywords: rss,cli,feeds,mcp,openrssgate
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: httpx==0.28.1
|
|
23
|
+
Requires-Dist: typer==0.16.1
|
|
24
|
+
|
|
25
|
+
# OpenRSSGate CLI
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
Local install with `pipx`:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd /Users/tomato/cursor/openrssgate/cli
|
|
33
|
+
pipx install .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
After PyPI release:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pipx install openrssgate
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
or:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install openrssgate
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Install directly from GitHub before the PyPI release:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pipx install "git+https://github.com/<owner>/openrssgate.git#subdirectory=cli"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Release steps are documented in [RELEASE.md](/Users/tomato/cursor/openrssgate/cli/RELEASE.md).
|
|
55
|
+
Homebrew tap packaging notes are documented in [homebrew/README.md](/Users/tomato/cursor/openrssgate/cli/homebrew/README.md).
|
|
56
|
+
External service setup steps are documented in [EXTERNAL_SETUP.md](/Users/tomato/cursor/openrssgate/cli/EXTERNAL_SETUP.md).
|
|
57
|
+
|
|
58
|
+
After Homebrew tap release:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
brew tap <owner>/tap
|
|
62
|
+
brew install openrssgate
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Run
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
openrssgate list
|
|
69
|
+
openrssgate feeds --since 7d
|
|
70
|
+
openrssgate stats
|
|
71
|
+
openrssgate validate https://example.com/rss.xml
|
|
72
|
+
openrssgate feed <feed_id>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Set `OPENRSSGATE_API_BASE_URL` to the backend base URL if needed.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
export OPENRSSGATE_API_BASE_URL=https://openrssgate-production.up.railway.app/v1
|
|
79
|
+
openrssgate list
|
|
80
|
+
openrssgate feeds --q openai --tag ai --since 7d
|
|
81
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# OpenRSSGate CLI
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
Local install with `pipx`:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd /Users/tomato/cursor/openrssgate/cli
|
|
9
|
+
pipx install .
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
After PyPI release:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pipx install openrssgate
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
or:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install openrssgate
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Install directly from GitHub before the PyPI release:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pipx install "git+https://github.com/<owner>/openrssgate.git#subdirectory=cli"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Release steps are documented in [RELEASE.md](/Users/tomato/cursor/openrssgate/cli/RELEASE.md).
|
|
31
|
+
Homebrew tap packaging notes are documented in [homebrew/README.md](/Users/tomato/cursor/openrssgate/cli/homebrew/README.md).
|
|
32
|
+
External service setup steps are documented in [EXTERNAL_SETUP.md](/Users/tomato/cursor/openrssgate/cli/EXTERNAL_SETUP.md).
|
|
33
|
+
|
|
34
|
+
After Homebrew tap release:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
brew tap <owner>/tap
|
|
38
|
+
brew install openrssgate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Run
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
openrssgate list
|
|
45
|
+
openrssgate feeds --since 7d
|
|
46
|
+
openrssgate stats
|
|
47
|
+
openrssgate validate https://example.com/rss.xml
|
|
48
|
+
openrssgate feed <feed_id>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Set `OPENRSSGATE_API_BASE_URL` to the backend base URL if needed.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
export OPENRSSGATE_API_BASE_URL=https://openrssgate-production.up.railway.app/v1
|
|
55
|
+
openrssgate list
|
|
56
|
+
openrssgate feeds --q openai --tag ai --since 7d
|
|
57
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from openrssgate.config import get_api_base_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApiError(RuntimeError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OpenRSSGateClient:
|
|
16
|
+
def __init__(self, base_url: str | None = None) -> None:
|
|
17
|
+
self.base_url = (base_url or get_api_base_url()).rstrip("/")
|
|
18
|
+
|
|
19
|
+
def _request(self, path: str, *, method: str = "GET", params: dict[str, Any] | None = None, json_body: Any = None) -> Any:
|
|
20
|
+
try:
|
|
21
|
+
response = httpx.request(method, f"{self.base_url}{path}", params=params, json=json_body, timeout=10.0)
|
|
22
|
+
response.raise_for_status()
|
|
23
|
+
except httpx.HTTPStatusError as exc:
|
|
24
|
+
payload = exc.response.json() if exc.response.headers.get("content-type", "").startswith("application/json") else {}
|
|
25
|
+
message = payload.get("error", {}).get("message", exc.response.text)
|
|
26
|
+
raise ApiError(message) from exc
|
|
27
|
+
except httpx.HTTPError as exc:
|
|
28
|
+
raise ApiError(str(exc)) from exc
|
|
29
|
+
return response.json()
|
|
30
|
+
|
|
31
|
+
def list_sources(self, **params: Any) -> dict[str, Any]:
|
|
32
|
+
return self._request("/sources", params=params)
|
|
33
|
+
|
|
34
|
+
def list_feeds(self, **params: Any) -> dict[str, Any]:
|
|
35
|
+
return self._request("/feeds", params=params)
|
|
36
|
+
|
|
37
|
+
def get_feed(self, feed_id: str) -> dict[str, Any]:
|
|
38
|
+
return self._request(f"/feeds/{feed_id}")
|
|
39
|
+
|
|
40
|
+
def get_stats(self) -> dict[str, Any]:
|
|
41
|
+
return self._request("/stats")
|
|
42
|
+
|
|
43
|
+
def validate_source(self, rss_url: str, **params: Any) -> dict[str, Any]:
|
|
44
|
+
payload = {"rss_url": rss_url, **params}
|
|
45
|
+
return self._request("/sources/validate", method="POST", json_body=payload)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def format_json(payload: Any) -> str:
|
|
49
|
+
return json.dumps(payload, ensure_ascii=False, indent=2)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from openrssgate.client import OpenRSSGateClient, format_json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(app: typer.Typer) -> None:
|
|
9
|
+
@app.command("feeds")
|
|
10
|
+
def feeds(
|
|
11
|
+
source_id: str | None = typer.Argument(default=None),
|
|
12
|
+
query: str | None = typer.Option(None, "--q"),
|
|
13
|
+
lang: str | None = typer.Option(None, "--lang"),
|
|
14
|
+
source_type: str | None = typer.Option(None, "--type"),
|
|
15
|
+
category: str | None = typer.Option(default=None),
|
|
16
|
+
tag: str | None = typer.Option(default=None),
|
|
17
|
+
since: str | None = typer.Option(default=None),
|
|
18
|
+
page: int = typer.Option(default=1, min=1),
|
|
19
|
+
limit: int = typer.Option(default=20, min=1, max=100),
|
|
20
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
21
|
+
) -> None:
|
|
22
|
+
client = OpenRSSGateClient()
|
|
23
|
+
payload = client.list_feeds(
|
|
24
|
+
source_id=source_id,
|
|
25
|
+
q=query,
|
|
26
|
+
language=lang,
|
|
27
|
+
type=source_type,
|
|
28
|
+
category=category,
|
|
29
|
+
tag=tag,
|
|
30
|
+
since=since,
|
|
31
|
+
page=page,
|
|
32
|
+
limit=limit,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if json_output:
|
|
36
|
+
typer.echo(format_json(payload))
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
items = payload.get("items", [])
|
|
40
|
+
if not items:
|
|
41
|
+
typer.echo("No feeds found.")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
for item in items:
|
|
45
|
+
typer.echo(item["title"])
|
|
46
|
+
typer.echo(f" id: {item['id']}")
|
|
47
|
+
typer.echo(f" source: {item['source_id']}")
|
|
48
|
+
typer.echo(f" published: {item.get('published_at') or '-'}")
|
|
49
|
+
typer.echo(f" url: {item['feed_url']}")
|
|
50
|
+
|
|
51
|
+
@app.command("feed")
|
|
52
|
+
def feed(
|
|
53
|
+
feed_id: str = typer.Argument(...),
|
|
54
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
55
|
+
) -> None:
|
|
56
|
+
client = OpenRSSGateClient()
|
|
57
|
+
payload = client.get_feed(feed_id)
|
|
58
|
+
|
|
59
|
+
if json_output:
|
|
60
|
+
typer.echo(format_json(payload))
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
source = payload.get("source", {})
|
|
64
|
+
typer.echo(payload["title"])
|
|
65
|
+
typer.echo(f" id: {payload['id']}")
|
|
66
|
+
typer.echo(f" source: {source.get('title') or payload.get('source_id')}")
|
|
67
|
+
typer.echo(f" source id: {payload['source_id']}")
|
|
68
|
+
typer.echo(f" published: {payload.get('published_at') or '-'}")
|
|
69
|
+
typer.echo(f" url: {payload['feed_url']}")
|
|
70
|
+
typer.echo(f" site: {source.get('site_url') or '-'}")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from openrssgate.client import OpenRSSGateClient, format_json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(app: typer.Typer) -> None:
|
|
9
|
+
@app.command("list")
|
|
10
|
+
def list_sources(
|
|
11
|
+
keyword: str | None = typer.Option(default=None),
|
|
12
|
+
lang: str | None = typer.Option(None, "--lang"),
|
|
13
|
+
source_type: str | None = typer.Option(None, "--type"),
|
|
14
|
+
category: str | None = typer.Option(default=None),
|
|
15
|
+
tag: str | None = typer.Option(default=None),
|
|
16
|
+
page: int = typer.Option(default=1, min=1),
|
|
17
|
+
limit: int = typer.Option(default=20, min=1, max=100),
|
|
18
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
19
|
+
) -> None:
|
|
20
|
+
client = OpenRSSGateClient()
|
|
21
|
+
payload = client.list_sources(
|
|
22
|
+
keyword=keyword,
|
|
23
|
+
language=lang,
|
|
24
|
+
type=source_type,
|
|
25
|
+
category=category,
|
|
26
|
+
tag=tag,
|
|
27
|
+
page=page,
|
|
28
|
+
limit=limit,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if json_output:
|
|
32
|
+
typer.echo(format_json(payload))
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
items = payload.get("items", [])
|
|
36
|
+
if not items:
|
|
37
|
+
typer.echo("No sources found.")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
for item in items:
|
|
41
|
+
tags = ", ".join(item.get("tags", [])) or "-"
|
|
42
|
+
categories = ", ".join(item.get("categories", [])) or "-"
|
|
43
|
+
typer.echo(
|
|
44
|
+
f"{item['title']} [{item.get('language') or '-'} / {item.get('type') or '-'} / {categories}]"
|
|
45
|
+
)
|
|
46
|
+
typer.echo(f" id: {item['id']}")
|
|
47
|
+
typer.echo(f" site: {item['site_url']}")
|
|
48
|
+
typer.echo(f" rss: {item['rss_url']}")
|
|
49
|
+
typer.echo(f" tags: {tags}")
|
|
50
|
+
typer.echo(f" last fetched: {item.get('last_fetched_at') or '-'}")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from openrssgate.client import OpenRSSGateClient, format_json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(app: typer.Typer) -> None:
|
|
9
|
+
@app.command("stats")
|
|
10
|
+
def stats(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
11
|
+
client = OpenRSSGateClient()
|
|
12
|
+
payload = client.get_stats()
|
|
13
|
+
|
|
14
|
+
if json_output:
|
|
15
|
+
typer.echo(format_json(payload))
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
typer.echo(f"total sources: {payload['total_sources']}")
|
|
19
|
+
typer.echo(f"active sources: {payload['active_sources']}")
|
|
20
|
+
typer.echo(f"total feeds: {payload['total_feeds']}")
|
|
21
|
+
typer.echo(f"feeds last 24h: {payload['feeds_last_24h']}")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from openrssgate.client import OpenRSSGateClient, format_json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(app: typer.Typer) -> None:
|
|
9
|
+
@app.command("validate")
|
|
10
|
+
def validate(
|
|
11
|
+
rss_url: str = typer.Argument(...),
|
|
12
|
+
lang: str | None = typer.Option(None, "--lang"),
|
|
13
|
+
source_type: str | None = typer.Option(None, "--type"),
|
|
14
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
15
|
+
) -> None:
|
|
16
|
+
client = OpenRSSGateClient()
|
|
17
|
+
payload = client.validate_source(rss_url, language=lang, type=source_type, categories=[], tags=[])
|
|
18
|
+
|
|
19
|
+
if json_output:
|
|
20
|
+
typer.echo(format_json(payload))
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
typer.echo(f"valid: {payload['valid']}")
|
|
24
|
+
typer.echo(f"title: {payload.get('title') or '-'}")
|
|
25
|
+
typer.echo(f"site: {payload.get('site_url') or '-'}")
|
|
26
|
+
typer.echo(f"rss: {payload['rss_url']}")
|
|
27
|
+
typer.echo(f"language: {payload.get('language') or '-'}")
|
|
28
|
+
typer.echo(f"type: {payload.get('type') or '-'}")
|
|
29
|
+
typer.echo(f"feed format: {payload.get('feed_format') or '-'}")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from openrssgate.client import ApiError
|
|
6
|
+
from openrssgate.commands import feeds, list as list_command, stats, validate
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(
|
|
9
|
+
add_completion=False,
|
|
10
|
+
no_args_is_help=True,
|
|
11
|
+
help="Read-only CLI for querying RSS Gateway sources and feeds.",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
list_command.register(app)
|
|
15
|
+
feeds.register(app)
|
|
16
|
+
stats.register(app)
|
|
17
|
+
validate.register(app)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.callback()
|
|
21
|
+
def main() -> None:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _run() -> None:
|
|
26
|
+
try:
|
|
27
|
+
app()
|
|
28
|
+
except ApiError as exc:
|
|
29
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
30
|
+
raise typer.Exit(code=1) from exc
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
_run()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openrssgate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only CLI for querying OpenRSSGate sources and feeds.
|
|
5
|
+
Author: OpenRSSGate
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/windbug99/openrssgate
|
|
8
|
+
Project-URL: Repository, https://github.com/windbug99/openrssgate
|
|
9
|
+
Project-URL: Issues, https://github.com/windbug99/openrssgate/issues
|
|
10
|
+
Keywords: rss,cli,feeds,mcp,openrssgate
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: httpx==0.28.1
|
|
23
|
+
Requires-Dist: typer==0.16.1
|
|
24
|
+
|
|
25
|
+
# OpenRSSGate CLI
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
Local install with `pipx`:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd /Users/tomato/cursor/openrssgate/cli
|
|
33
|
+
pipx install .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
After PyPI release:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pipx install openrssgate
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
or:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install openrssgate
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Install directly from GitHub before the PyPI release:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pipx install "git+https://github.com/<owner>/openrssgate.git#subdirectory=cli"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Release steps are documented in [RELEASE.md](/Users/tomato/cursor/openrssgate/cli/RELEASE.md).
|
|
55
|
+
Homebrew tap packaging notes are documented in [homebrew/README.md](/Users/tomato/cursor/openrssgate/cli/homebrew/README.md).
|
|
56
|
+
External service setup steps are documented in [EXTERNAL_SETUP.md](/Users/tomato/cursor/openrssgate/cli/EXTERNAL_SETUP.md).
|
|
57
|
+
|
|
58
|
+
After Homebrew tap release:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
brew tap <owner>/tap
|
|
62
|
+
brew install openrssgate
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Run
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
openrssgate list
|
|
69
|
+
openrssgate feeds --since 7d
|
|
70
|
+
openrssgate stats
|
|
71
|
+
openrssgate validate https://example.com/rss.xml
|
|
72
|
+
openrssgate feed <feed_id>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Set `OPENRSSGATE_API_BASE_URL` to the backend base URL if needed.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
export OPENRSSGATE_API_BASE_URL=https://openrssgate-production.up.railway.app/v1
|
|
79
|
+
openrssgate list
|
|
80
|
+
openrssgate feeds --q openai --tag ai --since 7d
|
|
81
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
openrssgate/__init__.py
|
|
5
|
+
openrssgate/client.py
|
|
6
|
+
openrssgate/config.py
|
|
7
|
+
openrssgate/main.py
|
|
8
|
+
openrssgate.egg-info/PKG-INFO
|
|
9
|
+
openrssgate.egg-info/SOURCES.txt
|
|
10
|
+
openrssgate.egg-info/dependency_links.txt
|
|
11
|
+
openrssgate.egg-info/entry_points.txt
|
|
12
|
+
openrssgate.egg-info/requires.txt
|
|
13
|
+
openrssgate.egg-info/top_level.txt
|
|
14
|
+
openrssgate/commands/__init__.py
|
|
15
|
+
openrssgate/commands/feeds.py
|
|
16
|
+
openrssgate/commands/list.py
|
|
17
|
+
openrssgate/commands/stats.py
|
|
18
|
+
openrssgate/commands/validate.py
|
|
19
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openrssgate
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "openrssgate"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Read-only CLI for querying OpenRSSGate sources and feeds."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "OpenRSSGate" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["rss", "cli", "feeds", "mcp", "openrssgate"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"httpx==0.28.1",
|
|
29
|
+
"typer==0.16.1",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/windbug99/openrssgate"
|
|
34
|
+
Repository = "https://github.com/windbug99/openrssgate"
|
|
35
|
+
Issues = "https://github.com/windbug99/openrssgate/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
openrssgate = "openrssgate.main:_run"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
include = ["openrssgate*"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
setup(
|
|
5
|
+
name="openrssgate",
|
|
6
|
+
version="0.1.0",
|
|
7
|
+
packages=find_packages(),
|
|
8
|
+
install_requires=[
|
|
9
|
+
"typer==0.16.1",
|
|
10
|
+
"httpx==0.28.1",
|
|
11
|
+
],
|
|
12
|
+
entry_points={
|
|
13
|
+
"console_scripts": [
|
|
14
|
+
"openrssgate=openrssgate.main:_run",
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from openrssgate.client import OpenRSSGateClient, format_json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_format_json_outputs_indented_unicode() -> None:
|
|
11
|
+
payload = {"title": "테스트"}
|
|
12
|
+
assert json.loads(format_json(payload)) == payload
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_list_sources_builds_expected_request(monkeypatch) -> None:
|
|
16
|
+
captured: dict[str, object] = {}
|
|
17
|
+
|
|
18
|
+
def fake_request(
|
|
19
|
+
method: str,
|
|
20
|
+
url: str,
|
|
21
|
+
params: dict[str, object] | None = None,
|
|
22
|
+
json: object | None = None,
|
|
23
|
+
timeout: float | None = None,
|
|
24
|
+
) -> httpx.Response:
|
|
25
|
+
captured["method"] = method
|
|
26
|
+
captured["url"] = url
|
|
27
|
+
captured["params"] = params
|
|
28
|
+
captured["json"] = json
|
|
29
|
+
request = httpx.Request("GET", url, params=params)
|
|
30
|
+
return httpx.Response(200, json={"items": [], "page": 1, "limit": 20, "total": 0}, request=request)
|
|
31
|
+
|
|
32
|
+
monkeypatch.setattr(httpx, "request", fake_request)
|
|
33
|
+
|
|
34
|
+
client = OpenRSSGateClient("http://127.0.0.1:8000/v1")
|
|
35
|
+
client.list_sources(language="ko", limit=10)
|
|
36
|
+
|
|
37
|
+
assert captured["method"] == "GET"
|
|
38
|
+
assert captured["url"] == "http://127.0.0.1:8000/v1/sources"
|
|
39
|
+
assert captured["params"] == {"language": "ko", "limit": 10}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_get_stats_builds_expected_request(monkeypatch) -> None:
|
|
43
|
+
captured: dict[str, object] = {}
|
|
44
|
+
|
|
45
|
+
def fake_request(
|
|
46
|
+
method: str,
|
|
47
|
+
url: str,
|
|
48
|
+
params: dict[str, object] | None = None,
|
|
49
|
+
json: object | None = None,
|
|
50
|
+
timeout: float | None = None,
|
|
51
|
+
) -> httpx.Response:
|
|
52
|
+
captured["method"] = method
|
|
53
|
+
captured["url"] = url
|
|
54
|
+
captured["params"] = params
|
|
55
|
+
captured["json"] = json
|
|
56
|
+
request = httpx.Request(method, url, params=params)
|
|
57
|
+
return httpx.Response(200, json={"total_sources": 1, "active_sources": 1, "total_feeds": 1, "feeds_last_24h": 1}, request=request)
|
|
58
|
+
|
|
59
|
+
monkeypatch.setattr(httpx, "request", fake_request)
|
|
60
|
+
|
|
61
|
+
client = OpenRSSGateClient("http://127.0.0.1:8000/v1")
|
|
62
|
+
client.get_stats()
|
|
63
|
+
|
|
64
|
+
assert captured["method"] == "GET"
|
|
65
|
+
assert captured["url"] == "http://127.0.0.1:8000/v1/stats"
|
|
66
|
+
assert captured["params"] is None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_validate_source_builds_expected_request(monkeypatch) -> None:
|
|
70
|
+
captured: dict[str, object] = {}
|
|
71
|
+
|
|
72
|
+
def fake_request(
|
|
73
|
+
method: str,
|
|
74
|
+
url: str,
|
|
75
|
+
params: dict[str, object] | None = None,
|
|
76
|
+
json: object | None = None,
|
|
77
|
+
timeout: float | None = None,
|
|
78
|
+
) -> httpx.Response:
|
|
79
|
+
captured["method"] = method
|
|
80
|
+
captured["url"] = url
|
|
81
|
+
captured["params"] = params
|
|
82
|
+
captured["json"] = json
|
|
83
|
+
request = httpx.Request(method, url, params=params)
|
|
84
|
+
return httpx.Response(200, json={"valid": True, "rss_url": "https://example.com/rss.xml"}, request=request)
|
|
85
|
+
|
|
86
|
+
monkeypatch.setattr(httpx, "request", fake_request)
|
|
87
|
+
|
|
88
|
+
client = OpenRSSGateClient("http://127.0.0.1:8000/v1")
|
|
89
|
+
client.validate_source("https://example.com/rss.xml", language="ko", type="blog", categories=[], tags=[])
|
|
90
|
+
|
|
91
|
+
assert captured["method"] == "POST"
|
|
92
|
+
assert captured["url"] == "http://127.0.0.1:8000/v1/sources/validate"
|
|
93
|
+
assert captured["json"] == {
|
|
94
|
+
"rss_url": "https://example.com/rss.xml",
|
|
95
|
+
"language": "ko",
|
|
96
|
+
"type": "blog",
|
|
97
|
+
"categories": [],
|
|
98
|
+
"tags": [],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_get_feed_builds_expected_request(monkeypatch) -> None:
|
|
103
|
+
captured: dict[str, object] = {}
|
|
104
|
+
|
|
105
|
+
def fake_request(
|
|
106
|
+
method: str,
|
|
107
|
+
url: str,
|
|
108
|
+
params: dict[str, object] | None = None,
|
|
109
|
+
json: object | None = None,
|
|
110
|
+
timeout: float | None = None,
|
|
111
|
+
) -> httpx.Response:
|
|
112
|
+
captured["method"] = method
|
|
113
|
+
captured["url"] = url
|
|
114
|
+
request = httpx.Request(method, url, params=params)
|
|
115
|
+
return httpx.Response(200, json={"id": "feed-1"}, request=request)
|
|
116
|
+
|
|
117
|
+
monkeypatch.setattr(httpx, "request", fake_request)
|
|
118
|
+
|
|
119
|
+
client = OpenRSSGateClient("http://127.0.0.1:8000/v1")
|
|
120
|
+
client.get_feed("feed-1")
|
|
121
|
+
|
|
122
|
+
assert captured["method"] == "GET"
|
|
123
|
+
assert captured["url"] == "http://127.0.0.1:8000/v1/feeds/feed-1"
|