openrssgate 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0"
openrssgate/client.py ADDED
@@ -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 '-'}")
openrssgate/config.py ADDED
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+
6
+ def get_api_base_url() -> str:
7
+ return os.getenv("OPENRSSGATE_API_BASE_URL", "http://127.0.0.1:8000/v1").rstrip("/")
openrssgate/main.py ADDED
@@ -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,14 @@
1
+ openrssgate/__init__.py,sha256=4t_crzhrLum--oyowUMxtjBTzUtWp7oRTF22ewEvJG4,49
2
+ openrssgate/client.py,sha256=b1Fi1T573HVVL4A6JeBuZ0FV-XvEmrlTit6jnIbxIzA,1780
3
+ openrssgate/config.py,sha256=b1HnCyAzEkvE_Kam_o6xKyiO6i8yeB87_g8iI9xyT30,168
4
+ openrssgate/main.py,sha256=QDnJod3zczx6zrbb3oCNhSAlzmBJui646g1MZO6IHq0,651
5
+ openrssgate/commands/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
+ openrssgate/commands/feeds.py,sha256=C3NViN8ieNMYNJ0AxQWh3ClNuiMXbqitxzRS1ljmxxU,2404
7
+ openrssgate/commands/list.py,sha256=pBuUe1I26vIV2KsTNg7PK3aOqch9OiRVKPytT6jvBPc,1748
8
+ openrssgate/commands/stats.py,sha256=d9kvk37E7C5vndyaDOJ2vx0C8TIHsKar34aDrX5n-mM,678
9
+ openrssgate/commands/validate.py,sha256=Jnmkg7kBog2lnLqK-IndGJgoMBXXuM6CuTzUyLBPBgI,1090
10
+ openrssgate-0.1.0.dist-info/METADATA,sha256=dRDcT6bMb0tl6MtIFnMXWQJfI36sD-23h-YLXixhutk,2196
11
+ openrssgate-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ openrssgate-0.1.0.dist-info/entry_points.txt,sha256=MT_PBta0sarV-zvWR0YbcJKNGmDKxEyP4MwTnFoM0bw,54
13
+ openrssgate-0.1.0.dist-info/top_level.txt,sha256=JDeXyYvFnu8S6h6loVQ8iN3nlvqUm1bmp-u-OrgTzwQ,12
14
+ openrssgate-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ openrssgate = openrssgate.main:_run
@@ -0,0 +1 @@
1
+ openrssgate