geonode-scraper-cli 0.1.0__tar.gz → 0.2.1__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.
Files changed (34) hide show
  1. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/PKG-INFO +9 -4
  2. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/README.md +7 -2
  3. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/__init__.py +1 -1
  4. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/commands/map.py +1 -1
  5. geonode_scraper_cli-0.2.1/geonode_scraper_cli/commands/search.py +74 -0
  6. geonode_scraper_cli-0.2.1/geonode_scraper_cli/commands/usage.py +21 -0
  7. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/config.py +7 -1
  8. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/context.py +3 -1
  9. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/main.py +4 -0
  10. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli.egg-info/PKG-INFO +9 -4
  11. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli.egg-info/SOURCES.txt +4 -1
  12. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli.egg-info/requires.txt +1 -1
  13. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/pyproject.toml +2 -2
  14. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/tests/test_cli.py +50 -1
  15. geonode_scraper_cli-0.2.1/tests/test_user_agent.py +28 -0
  16. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/commands/__init__.py +0 -0
  17. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/commands/_common.py +0 -0
  18. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/commands/batch.py +0 -0
  19. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/commands/config.py +0 -0
  20. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/commands/crawl.py +0 -0
  21. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/commands/extract.py +0 -0
  22. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/commands/jobs.py +0 -0
  23. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/commands/stats.py +0 -0
  24. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/commands/system.py +0 -0
  25. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/errors.py +0 -0
  26. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/invoke.py +0 -0
  27. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/output.py +0 -0
  28. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/py.typed +0 -0
  29. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/renderers/__init__.py +0 -0
  30. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli/renderers/tables.py +0 -0
  31. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli.egg-info/dependency_links.txt +0 -0
  32. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli.egg-info/entry_points.txt +0 -0
  33. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/geonode_scraper_cli.egg-info/top_level.txt +0 -0
  34. {geonode_scraper_cli-0.1.0 → geonode_scraper_cli-0.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geonode-scraper-cli
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Command-line interface for the Geonode Scraper API
5
5
  Author: Geonode Team
6
6
  License-Expression: MIT
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Typing :: Typed
11
11
  Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
- Requires-Dist: geonode-scraper-tools-core>=0.3.1
13
+ Requires-Dist: geonode-scraper-tools-core>=0.5.0
14
14
  Requires-Dist: typer>=0.12
15
15
  Requires-Dist: rich>=13.0
16
16
  Requires-Dist: PyYAML>=6.0
@@ -129,11 +129,16 @@ gscraper crawl wait JOB_ID
129
129
  gscraper crawl list [--url ...]
130
130
  gscraper crawl cancel JOB_ID
131
131
 
132
- gscraper map run URL [--search term] [--no-subdomains] # primary action
133
- gscraper map jobs list # inspect past map jobs
132
+ gscraper map run URL [--search term] [--include-subdomains] # primary action
133
+ gscraper map jobs list # inspect past map jobs
134
134
  gscraper map jobs get JOB_ID
135
135
 
136
+ gscraper search run QUERY [--locale en] [--page 1] [--safe off] [--time-range week]
137
+ gscraper search jobs list [--query ...] [--status completed]
138
+ gscraper search jobs get JOB_ID
139
+
136
140
  gscraper stats [--start-date ISO] [--end-date ISO]
141
+ gscraper usage concurrency
137
142
  gscraper health
138
143
  ```
139
144
 
@@ -107,11 +107,16 @@ gscraper crawl wait JOB_ID
107
107
  gscraper crawl list [--url ...]
108
108
  gscraper crawl cancel JOB_ID
109
109
 
110
- gscraper map run URL [--search term] [--no-subdomains] # primary action
111
- gscraper map jobs list # inspect past map jobs
110
+ gscraper map run URL [--search term] [--include-subdomains] # primary action
111
+ gscraper map jobs list # inspect past map jobs
112
112
  gscraper map jobs get JOB_ID
113
113
 
114
+ gscraper search run QUERY [--locale en] [--page 1] [--safe off] [--time-range week]
115
+ gscraper search jobs list [--query ...] [--status completed]
116
+ gscraper search jobs get JOB_ID
117
+
114
118
  gscraper stats [--start-date ISO] [--end-date ISO]
119
+ gscraper usage concurrency
115
120
  gscraper health
116
121
  ```
117
122
 
@@ -1,3 +1,3 @@
1
- __version__ = "0.1.0"
1
+ __version__ = "0.2.1"
2
2
 
3
3
  __all__ = ["__version__"]
@@ -20,7 +20,7 @@ def run(
20
20
  ctx: typer.Context,
21
21
  url: str = typer.Argument(..., help="Base URL to discover links from."),
22
22
  search: Optional[str] = typer.Option(None, "--search", help="Filter discovered links by substring."),
23
- include_subdomains: bool = typer.Option(True, "--include-subdomains/--no-subdomains", help="Include subdomains."),
23
+ include_subdomains: bool = typer.Option(False, "--include-subdomains/--no-subdomains", help="Include subdomains."),
24
24
  ignore_query_parameters: bool = typer.Option(
25
25
  True, "--ignore-query/--keep-query", help="Ignore query parameters when de-duplicating links."
26
26
  ),
@@ -0,0 +1,74 @@
1
+ """`gscraper search` — run a search query and inspect search jobs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from geonode_scraper_tools_core.schemas import GetSearchJobInput, ListSearchJobsInput, SearchInput
9
+
10
+ from ..invoke import call, validated
11
+ from ._common import apply_output_override, optional
12
+
13
+ search_app = typer.Typer(help="Run search queries and inspect search jobs.", no_args_is_help=True)
14
+ search_jobs_app = typer.Typer(help="Inspect search jobs.", no_args_is_help=True)
15
+ search_app.add_typer(search_jobs_app, name="jobs")
16
+
17
+
18
+ @search_app.command("run")
19
+ def run(
20
+ ctx: typer.Context,
21
+ query: str = typer.Argument(..., help="Search query."),
22
+ locale: Optional[str] = typer.Option(None, "--locale", help="Locale tag, e.g. 'en' or 'en-US'."),
23
+ page: int = typer.Option(1, "--page", help="Result page to fetch (1-20)."),
24
+ safe: str = typer.Option("off", "--safe", help="Safe-search level: off|moderate|strict."),
25
+ time_range: Optional[str] = typer.Option(None, "--time-range", help="day|week|month|year."),
26
+ as_json: bool = typer.Option(False, "--json", help="Output raw JSON envelope."),
27
+ as_yaml: bool = typer.Option(False, "--yaml", help="Output raw YAML envelope."),
28
+ ) -> None:
29
+ """Run a search query (returns synchronously)."""
30
+ apply_output_override(ctx, as_json, as_yaml)
31
+ payload = validated(
32
+ SearchInput,
33
+ query=query,
34
+ page=page,
35
+ safe=safe,
36
+ **optional(locale=locale, time_range=time_range),
37
+ )
38
+ call(ctx, "search", payload)
39
+
40
+
41
+ @search_jobs_app.command("list")
42
+ def list_jobs(
43
+ ctx: typer.Context,
44
+ query: Optional[str] = typer.Option(None, "--query", help="Filter by search query."),
45
+ status: Optional[str] = typer.Option(None, "--status", help="queued|processing|completed|failed|cancelled."),
46
+ start_date: Optional[str] = typer.Option(None, "--start-date", help="ISO 8601 lower bound."),
47
+ end_date: Optional[str] = typer.Option(None, "--end-date", help="ISO 8601 upper bound."),
48
+ page: int = typer.Option(1, "--page", help="Page number."),
49
+ page_size: int = typer.Option(100, "--page-size", help="Results per page (max 100)."),
50
+ as_json: bool = typer.Option(False, "--json", help="Output raw JSON envelope."),
51
+ as_yaml: bool = typer.Option(False, "--yaml", help="Output raw YAML envelope."),
52
+ ) -> None:
53
+ """List and filter search jobs."""
54
+ apply_output_override(ctx, as_json, as_yaml)
55
+ payload = validated(
56
+ ListSearchJobsInput,
57
+ page=page,
58
+ page_size=page_size,
59
+ **optional(query=query, status=status, start_date=start_date, end_date=end_date),
60
+ )
61
+ call(ctx, "list_search_jobs", payload)
62
+
63
+
64
+ @search_jobs_app.command("get")
65
+ def get(
66
+ ctx: typer.Context,
67
+ job_id: str = typer.Argument(..., help="Search job ID."),
68
+ as_json: bool = typer.Option(False, "--json", help="Output raw JSON envelope."),
69
+ as_yaml: bool = typer.Option(False, "--yaml", help="Output raw YAML envelope."),
70
+ ) -> None:
71
+ """Fetch a single search job by ID."""
72
+ apply_output_override(ctx, as_json, as_yaml)
73
+ payload = validated(GetSearchJobInput, job_id=job_id)
74
+ call(ctx, "get_search_job", payload)
@@ -0,0 +1,21 @@
1
+ """`gscraper usage` — plan usage and limits."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from ..invoke import call
8
+ from ._common import apply_output_override
9
+
10
+ usage_app = typer.Typer(help="Usage and limits.", no_args_is_help=True)
11
+
12
+
13
+ @usage_app.command("concurrency")
14
+ def concurrency(
15
+ ctx: typer.Context,
16
+ as_json: bool = typer.Option(False, "--json", help="Output raw JSON envelope."),
17
+ as_yaml: bool = typer.Option(False, "--yaml", help="Output raw YAML envelope."),
18
+ ) -> None:
19
+ """Check the caller's live work-concurrency slot usage against their plan limit."""
20
+ apply_output_override(ctx, as_json, as_yaml)
21
+ call(ctx, "get_concurrency_usage", {})
@@ -15,7 +15,13 @@ from geonode_scraper_tools_core import ScraperToolSettings
15
15
 
16
16
  ENV_PREFIX = "GEONODE_SCRAPER_"
17
17
  DEFAULT_PROFILE = "default"
18
- DEFAULT_HOST = "http://localhost"
18
+ # The production API. It used to be `http://localhost`, which is the OpenAPI
19
+ # generator's fallback for a spec with no `servers` block - harmless in a generated
20
+ # client that documents it, and wrong in a CLI a person installs and runs, where it
21
+ # means every command fails to connect until a host is configured. A flag, the
22
+ # environment or the config file still override it; this is only what happens when
23
+ # nobody said anything.
24
+ DEFAULT_HOST = "https://scraper.geonode.io"
19
25
 
20
26
 
21
27
  class ConfigError(Exception):
@@ -7,6 +7,7 @@ from dataclasses import dataclass
7
7
  import typer
8
8
  from geonode_scraper_tools_core import ScraperToolService, ScraperToolSettings
9
9
 
10
+ from . import __version__
10
11
  from .config import ConfigError, resolve_settings
11
12
  from .errors import ExitCode
12
13
  from .output import OutputFormat
@@ -49,7 +50,8 @@ class CLIContext:
49
50
 
50
51
  def service(self) -> ScraperToolService:
51
52
  if self._service is None:
52
- self._service = ScraperToolService(self.settings())
53
+ # The API labels requests from this package `cli` (scraper-api#674).
54
+ self._service = ScraperToolService(self.settings(), client=f"geonode-cli/{__version__}")
53
55
  return self._service
54
56
 
55
57
 
@@ -13,8 +13,10 @@ from .commands.crawl import crawl_app
13
13
  from .commands.extract import extract
14
14
  from .commands.jobs import jobs_app
15
15
  from .commands.map import map_app
16
+ from .commands.search import search_app
16
17
  from .commands.stats import stats
17
18
  from .commands.system import health
19
+ from .commands.usage import usage_app
18
20
  from .context import CLIContext
19
21
  from .output import OutputFormat
20
22
 
@@ -76,6 +78,8 @@ app.add_typer(jobs_app, name="jobs")
76
78
  app.add_typer(batch_app, name="batch")
77
79
  app.add_typer(crawl_app, name="crawl")
78
80
  app.add_typer(map_app, name="map")
81
+ app.add_typer(search_app, name="search")
82
+ app.add_typer(usage_app, name="usage")
79
83
  app.add_typer(config_app, name="config")
80
84
 
81
85
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geonode-scraper-cli
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Command-line interface for the Geonode Scraper API
5
5
  Author: Geonode Team
6
6
  License-Expression: MIT
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Typing :: Typed
11
11
  Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
- Requires-Dist: geonode-scraper-tools-core>=0.3.1
13
+ Requires-Dist: geonode-scraper-tools-core>=0.5.0
14
14
  Requires-Dist: typer>=0.12
15
15
  Requires-Dist: rich>=13.0
16
16
  Requires-Dist: PyYAML>=6.0
@@ -129,11 +129,16 @@ gscraper crawl wait JOB_ID
129
129
  gscraper crawl list [--url ...]
130
130
  gscraper crawl cancel JOB_ID
131
131
 
132
- gscraper map run URL [--search term] [--no-subdomains] # primary action
133
- gscraper map jobs list # inspect past map jobs
132
+ gscraper map run URL [--search term] [--include-subdomains] # primary action
133
+ gscraper map jobs list # inspect past map jobs
134
134
  gscraper map jobs get JOB_ID
135
135
 
136
+ gscraper search run QUERY [--locale en] [--page 1] [--safe off] [--time-range week]
137
+ gscraper search jobs list [--query ...] [--status completed]
138
+ gscraper search jobs get JOB_ID
139
+
136
140
  gscraper stats [--start-date ISO] [--end-date ISO]
141
+ gscraper usage concurrency
137
142
  gscraper health
138
143
  ```
139
144
 
@@ -22,8 +22,11 @@ geonode_scraper_cli/commands/crawl.py
22
22
  geonode_scraper_cli/commands/extract.py
23
23
  geonode_scraper_cli/commands/jobs.py
24
24
  geonode_scraper_cli/commands/map.py
25
+ geonode_scraper_cli/commands/search.py
25
26
  geonode_scraper_cli/commands/stats.py
26
27
  geonode_scraper_cli/commands/system.py
28
+ geonode_scraper_cli/commands/usage.py
27
29
  geonode_scraper_cli/renderers/__init__.py
28
30
  geonode_scraper_cli/renderers/tables.py
29
- tests/test_cli.py
31
+ tests/test_cli.py
32
+ tests/test_user_agent.py
@@ -1,4 +1,4 @@
1
- geonode-scraper-tools-core>=0.3.1
1
+ geonode-scraper-tools-core>=0.5.0
2
2
  typer>=0.12
3
3
  rich>=13.0
4
4
  PyYAML>=6.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geonode-scraper-cli"
3
- version = "0.1.0"
3
+ version = "0.2.1"
4
4
  description = "Command-line interface for the Geonode Scraper API"
5
5
  authors = [
6
6
  {name = "Geonode Team"},
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Typing :: Typed",
16
16
  ]
17
17
  dependencies = [
18
- "geonode-scraper-tools-core>=0.3.1",
18
+ "geonode-scraper-tools-core>=0.5.0",
19
19
  "typer>=0.12",
20
20
  "rich>=13.0",
21
21
  "PyYAML>=6.0",
@@ -97,7 +97,7 @@ def test_missing_api_key_is_usage_error(invoke, fake_service):
97
97
  def test_flag_overrides_env_host(invoke, fake_service, monkeypatch):
98
98
  captured: dict[str, object] = {}
99
99
 
100
- def _capture(settings):
100
+ def _capture(settings, **_):
101
101
  captured["host"] = settings.host
102
102
  return fake_service
103
103
 
@@ -131,6 +131,55 @@ def test_map_run_renders_links(invoke, fake_service):
131
131
  assert "https://a.com" in result.stdout
132
132
 
133
133
 
134
+ def test_map_run_defaults_include_subdomains_false(invoke, fake_service):
135
+ invoke(["map", "run", "https://example.com"])
136
+ _, kwargs = fake_service.calls[-1]
137
+ assert kwargs["include_subdomains"] is False
138
+
139
+
140
+ def test_search_run_passes_validated_payload(invoke, fake_service):
141
+ fake_service.responses["search"] = {
142
+ "ok": True,
143
+ "operation": "search",
144
+ "result": {"job_id": "00000000-0000-0000-0000-000000000007", "results": []},
145
+ }
146
+ result = invoke(["search", "run", "geonode scraper api", "--page", "2", "--safe", "moderate"])
147
+ assert result.exit_code == 0
148
+ name, kwargs = fake_service.calls[-1]
149
+ assert name == "search"
150
+ assert kwargs["query"] == "geonode scraper api"
151
+ assert kwargs["page"] == 2
152
+ assert kwargs["safe"] == "moderate"
153
+
154
+
155
+ def test_search_jobs_list_calls_service(invoke, fake_service):
156
+ invoke(["search", "jobs", "list", "--query", "geonode", "--status", "completed"])
157
+ name, kwargs = fake_service.calls[-1]
158
+ assert name == "list_search_jobs"
159
+ assert kwargs["query"] == "geonode"
160
+ assert kwargs["status"] == "completed"
161
+
162
+
163
+ def test_search_jobs_get_calls_service(invoke, fake_service):
164
+ invoke(["search", "jobs", "get", "00000000-0000-0000-0000-000000000008"])
165
+ name, kwargs = fake_service.calls[-1]
166
+ assert name == "get_search_job"
167
+ assert kwargs == {"job_id": "00000000-0000-0000-0000-000000000008"}
168
+
169
+
170
+ def test_usage_concurrency_calls_service(invoke, fake_service):
171
+ fake_service.responses["get_concurrency_usage"] = {
172
+ "ok": True,
173
+ "operation": "get_concurrency_usage",
174
+ "result": {"work_concurrency_in_use": 2, "work_concurrency_limit": 10},
175
+ }
176
+ result = invoke(["usage", "concurrency"])
177
+ assert result.exit_code == 0
178
+ name, kwargs = fake_service.calls[-1]
179
+ assert name == "get_concurrency_usage"
180
+ assert kwargs == {}
181
+
182
+
134
183
  def test_config_path(invoke):
135
184
  result = invoke(["config", "path"], with_key=False)
136
185
  assert result.exit_code == 0
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import urllib3
7
+
8
+ HEALTH = {"version": "0.1.0", "service": "scraper-api", "timestamp": "2026-01-01T00:00:00Z", "status": "ok"}
9
+
10
+
11
+ def test_requests_name_the_cli_first(runner, tmp_path):
12
+ # The API labels a request by the first User-Agent token (scraper-api#674).
13
+ from geonode_scraper_cli import __version__
14
+ from geonode_scraper_cli.main import app
15
+
16
+ response = MagicMock(status=200, reason="OK", data=json.dumps(HEALTH).encode())
17
+ response.headers = {"content-type": "application/json"}
18
+ env = {
19
+ "GEONODE_SCRAPER_CONFIG": str(tmp_path / "config.toml"),
20
+ "GEONODE_SCRAPER_API_KEY": "test-key",
21
+ "GEONODE_SCRAPER_HOST": "http://api.mock.local",
22
+ }
23
+
24
+ with patch.object(urllib3.PoolManager, "request", return_value=response) as request:
25
+ result = runner.invoke(app, ["--json", "health"], env=env)
26
+
27
+ assert result.exit_code == 0, result.output
28
+ assert request.call_args.kwargs["headers"]["User-Agent"].split()[0] == f"geonode-cli/{__version__}"