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.
@@ -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,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0"
@@ -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,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,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("/")
@@ -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,2 @@
1
+ [console_scripts]
2
+ openrssgate = openrssgate.main:_run
@@ -0,0 +1,2 @@
1
+ httpx==0.28.1
2
+ typer==0.16.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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"