worai 1.0.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.
Files changed (59) hide show
  1. worai-1.0.0/PKG-INFO +120 -0
  2. worai-1.0.0/README.md +103 -0
  3. worai-1.0.0/pyproject.toml +34 -0
  4. worai-1.0.0/setup.cfg +4 -0
  5. worai-1.0.0/tests/test_canonicalize_duplicate_pages.py +27 -0
  6. worai-1.0.0/tests/test_config.py +32 -0
  7. worai-1.0.0/tests/test_dedupe.py +14 -0
  8. worai-1.0.0/tests/test_find_missing_names.py +24 -0
  9. worai-1.0.0/tests/test_find_url_by_type.py +28 -0
  10. worai-1.0.0/worai/__init__.py +4 -0
  11. worai-1.0.0/worai/cli.py +79 -0
  12. worai-1.0.0/worai/commands/__init__.py +1 -0
  13. worai-1.0.0/worai/commands/canonicalize_duplicate_pages.py +39 -0
  14. worai-1.0.0/worai/commands/dedupe.py +33 -0
  15. worai-1.0.0/worai/commands/delete_entities_from_csv.py +25 -0
  16. worai-1.0.0/worai/commands/find_faq_page_wrong_type.py +41 -0
  17. worai-1.0.0/worai/commands/find_missing_names.py +35 -0
  18. worai-1.0.0/worai/commands/find_url_by_type.py +36 -0
  19. worai-1.0.0/worai/commands/google_search_console.py +65 -0
  20. worai-1.0.0/worai/commands/link_groups.py +43 -0
  21. worai-1.0.0/worai/commands/patch.py +40 -0
  22. worai-1.0.0/worai/commands/seocheck.py +80 -0
  23. worai-1.0.0/worai/commands/upload_entities_from_turtle.py +48 -0
  24. worai-1.0.0/worai/config.py +86 -0
  25. worai-1.0.0/worai/core/__init__.py +1 -0
  26. worai-1.0.0/worai/core/canonicalize_duplicate_pages.py +179 -0
  27. worai-1.0.0/worai/core/dedupe.py +166 -0
  28. worai-1.0.0/worai/core/delete_entities_from_csv.py +45 -0
  29. worai-1.0.0/worai/core/find_faq_page_wrong_type.py +154 -0
  30. worai-1.0.0/worai/core/find_missing_names.py +31 -0
  31. worai-1.0.0/worai/core/find_url_by_type.py +50 -0
  32. worai-1.0.0/worai/core/gsc.py +224 -0
  33. worai-1.0.0/worai/core/link_groups.py +255 -0
  34. worai-1.0.0/worai/core/patch.py +178 -0
  35. worai-1.0.0/worai/core/upload_entities_from_turtle.py +134 -0
  36. worai-1.0.0/worai/core/wordlift.py +78 -0
  37. worai-1.0.0/worai/errors.py +29 -0
  38. worai-1.0.0/worai/logging.py +28 -0
  39. worai-1.0.0/worai/seocheck/__init__.py +1 -0
  40. worai-1.0.0/worai/seocheck/browser.py +76 -0
  41. worai-1.0.0/worai/seocheck/checks/__init__.py +29 -0
  42. worai-1.0.0/worai/seocheck/checks/canonical.py +26 -0
  43. worai-1.0.0/worai/seocheck/checks/llms_txt.py +53 -0
  44. worai-1.0.0/worai/seocheck/checks/page_meta.py +54 -0
  45. worai-1.0.0/worai/seocheck/checks/resource_404.py +40 -0
  46. worai-1.0.0/worai/seocheck/checks/response_time.py +25 -0
  47. worai-1.0.0/worai/seocheck/checks/robots_txt.py +82 -0
  48. worai-1.0.0/worai/seocheck/checks/status.py +23 -0
  49. worai-1.0.0/worai/seocheck/checks/ttfb.py +72 -0
  50. worai-1.0.0/worai/seocheck/checks/types.py +19 -0
  51. worai-1.0.0/worai/seocheck/checks/wordlift_bootstrap.py +46 -0
  52. worai-1.0.0/worai/seocheck/cli.py +601 -0
  53. worai-1.0.0/worai/seocheck/sitemap.py +103 -0
  54. worai-1.0.0/worai.egg-info/PKG-INFO +120 -0
  55. worai-1.0.0/worai.egg-info/SOURCES.txt +101 -0
  56. worai-1.0.0/worai.egg-info/dependency_links.txt +1 -0
  57. worai-1.0.0/worai.egg-info/entry_points.txt +2 -0
  58. worai-1.0.0/worai.egg-info/requires.txt +11 -0
  59. worai-1.0.0/worai.egg-info/top_level.txt +1 -0
worai-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: worai
3
+ Version: 1.0.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: google-auth>=2.35.0
8
+ Requires-Dist: google-auth-oauthlib>=1.2.1
9
+ Requires-Dist: playwright>=1.48.0
10
+ Requires-Dist: rdflib>=7.5.0
11
+ Requires-Dist: requests>=2.32.5
12
+ Requires-Dist: tqdm>=4.67.1
13
+ Requires-Dist: typer>=0.12.5
14
+ Requires-Dist: wordlift-client>=1.133.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8.3.4; extra == "dev"
17
+
18
+ This folder contains WORAi CLI utilities.
19
+
20
+ Install
21
+ - Local: `pipx install .`
22
+ - From git: `pipx install git+<repo_url>`
23
+ - Curl installer: `curl -fsSL <install.sh_url> | bash -s -- <repo_url>`
24
+
25
+ Usage
26
+ - `worai --help`
27
+ - `worai seocheck <sitemap_url_or_path>`
28
+ - `worai google-search-console --site sc-domain:example.com --client-secrets /path/to/client_secrets.json`
29
+
30
+ Configuration
31
+ - Config file (TOML) discovery order: `--config`, `WORAI_CONFIG`, `./worai.toml`, `~/.config/worai/config.toml`, `~/.worai.toml`.
32
+ - Profiles: `[profile.<name>]` with `--profile` or `WORAI_PROFILE`.
33
+ - Common keys:
34
+ - `wordlift.api_key`
35
+ - `gsc.client_secrets`
36
+ - `gsc.token`
37
+
38
+ seocheck
39
+ The `seocheck` command runs SEO checks against URLs found in a sitemap. It supports remote sitemap URLs and local sitemap files.
40
+
41
+ Usage
42
+ - `worai seocheck <sitemap_url_or_path>`
43
+
44
+ Options
45
+ - `sitemap_url`: URL or local file path to a sitemap XML (or sitemap index). Supports `.gz` files and `file://` URLs.
46
+ - `--max-urls`: Limit the number of URLs checked. Default: no limit.
47
+ - `--timeout`: Timeout in seconds for HTTP requests (sitemaps, robots.txt, llms.txt). Default: 20.0.
48
+ - `--page-timeout`: Timeout in milliseconds for browser page loads. Default: 30000.
49
+ - `--wait-until`: Playwright wait strategy for page load. Choices: `domcontentloaded`, `load`, `networkidle`. Default: `domcontentloaded`.
50
+ - `--ttfb-ok-ms`: TTFB ok threshold in ms. Default: 200.
51
+ - `--ttfb-warn-ms`: TTFB warn threshold in ms. Default: 500.
52
+ - `--headed`: Run the browser with a visible UI instead of headless mode.
53
+ - `--format`: Output format. Choices: `text`, `json`. Default: `text`.
54
+ - `--output-dir`: Write report outputs to this directory (report.json, summary.txt, per-page JSONs, report UI).
55
+ - `--output`: Write a comprehensive JSON report to this file path.
56
+ - `--output-summary`: Write a human-readable summary report to this file path.
57
+ - `--save-html`: Save rendered HTML for each page to the output directory.
58
+ - `--checks`: Comma-separated list of page check names to run (others disabled).
59
+ - `--disable-checks`: Comma-separated list of page check names to skip.
60
+ - `--concurrency`: Number of pages to process concurrently, or `auto`. Default: 1.
61
+
62
+ Notes
63
+ - The page checks are modular. Add a new check under `worai/seocheck/checks` and register it in `worai/seocheck/checks/__init__.py`.
64
+ - The browser is Playwright (Chromium) to ensure JavaScript rendering.
65
+ - When using `--output-dir`, open `index.html` from that directory to view the report UI (or serve it with a web server).
66
+
67
+ Examples
68
+ - `worai seocheck https://example.com/sitemap.xml`
69
+ - `worai seocheck ./sitemap.xml`
70
+ - `worai seocheck /path/to/sitemap.xml.gz`
71
+ - `worai seocheck https://example.com/sitemap.xml --wait-until networkidle`
72
+ - `worai seocheck https://example.com/sitemap.xml --max-urls 25 --format json`
73
+ - `worai seocheck https://example.com/sitemap.xml --output ./report.json --output-summary ./report.txt`
74
+ - `worai seocheck https://example.com/sitemap.xml --output-dir ./seocheck-report --save-html`
75
+ - `worai seocheck https://example.com/sitemap.xml --checks status,page_meta,canonical`
76
+
77
+ Command Examples
78
+ - `worai google-search-console --site sc-domain:example.com --client-secrets ./client_secrets.json`
79
+ - `worai dedupe --dry-run`
80
+ - `worai canonicalize-duplicate-pages --input gsc_pages.csv --output canonical_targets.csv --kpi-window 28d --kpi-metric clicks`
81
+ - `worai delete-entities-from-csv ./entities.csv --batch-size 20`
82
+ - `worai find-faq-page-wrong-type ./data.ttl`
83
+ - `worai find-faq-page-wrong-type ./data.ttl --dry-run --replace-type`
84
+ - `worai find-faq-page-wrong-type ./data.ttl --patch --replace-type`
85
+ - `worai find-missing-names ./data.ttl`
86
+ - `worai find-url-by-type ./data.ttl schema:Service schema:Product`
87
+ - `worai link-groups ./links.csv --format turtle`
88
+ - `worai link-groups ./links.csv --apply --dry-run --concurrency 4`
89
+ - `worai patch ./data.ttl --dry-run --add-types`
90
+ - `worai patch ./data.jsonld --types-only --workers 4`
91
+ - `worai upload-entities-from-turtle ./entities --recursive --limit 50`
92
+
93
+ Client secrets format
94
+ The `--client-secrets` file is the OAuth2 client configuration downloaded from Google Cloud Console.
95
+ It looks like:
96
+
97
+ {
98
+ "installed": {
99
+ "client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
100
+ "project_id": "your-project-id",
101
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
102
+ "token_uri": "https://oauth2.googleapis.com/token",
103
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
104
+ "client_secret": "YOUR_CLIENT_SECRET",
105
+ "redirect_uris": [
106
+ "http://localhost"
107
+ ]
108
+ }
109
+ }
110
+
111
+ Setup steps
112
+ 1. Google Cloud Console -> APIs & Services -> OAuth consent screen:
113
+ - Configure a consent screen (External or Internal) and add yourself as a test user if required.
114
+ 2. APIs & Services -> Credentials -> Create Credentials -> OAuth client ID:
115
+ - Application type: Desktop app.
116
+ 3. Download the JSON and pass its path to `--client-secrets`.
117
+ 4. Run the command:
118
+ - `worai google-search-console --site sc-domain:example.com --client-secrets /path/to/client_secrets.json`
119
+ - If your OAuth client requires a fixed redirect URI (e.g. Web app), add `--port 8080` and register `http://localhost:8080`.
120
+ 5. The first run opens a browser for consent and writes a token file (default `gsc_token.json`).
worai-1.0.0/README.md ADDED
@@ -0,0 +1,103 @@
1
+ This folder contains WORAi CLI utilities.
2
+
3
+ Install
4
+ - Local: `pipx install .`
5
+ - From git: `pipx install git+<repo_url>`
6
+ - Curl installer: `curl -fsSL <install.sh_url> | bash -s -- <repo_url>`
7
+
8
+ Usage
9
+ - `worai --help`
10
+ - `worai seocheck <sitemap_url_or_path>`
11
+ - `worai google-search-console --site sc-domain:example.com --client-secrets /path/to/client_secrets.json`
12
+
13
+ Configuration
14
+ - Config file (TOML) discovery order: `--config`, `WORAI_CONFIG`, `./worai.toml`, `~/.config/worai/config.toml`, `~/.worai.toml`.
15
+ - Profiles: `[profile.<name>]` with `--profile` or `WORAI_PROFILE`.
16
+ - Common keys:
17
+ - `wordlift.api_key`
18
+ - `gsc.client_secrets`
19
+ - `gsc.token`
20
+
21
+ seocheck
22
+ The `seocheck` command runs SEO checks against URLs found in a sitemap. It supports remote sitemap URLs and local sitemap files.
23
+
24
+ Usage
25
+ - `worai seocheck <sitemap_url_or_path>`
26
+
27
+ Options
28
+ - `sitemap_url`: URL or local file path to a sitemap XML (or sitemap index). Supports `.gz` files and `file://` URLs.
29
+ - `--max-urls`: Limit the number of URLs checked. Default: no limit.
30
+ - `--timeout`: Timeout in seconds for HTTP requests (sitemaps, robots.txt, llms.txt). Default: 20.0.
31
+ - `--page-timeout`: Timeout in milliseconds for browser page loads. Default: 30000.
32
+ - `--wait-until`: Playwright wait strategy for page load. Choices: `domcontentloaded`, `load`, `networkidle`. Default: `domcontentloaded`.
33
+ - `--ttfb-ok-ms`: TTFB ok threshold in ms. Default: 200.
34
+ - `--ttfb-warn-ms`: TTFB warn threshold in ms. Default: 500.
35
+ - `--headed`: Run the browser with a visible UI instead of headless mode.
36
+ - `--format`: Output format. Choices: `text`, `json`. Default: `text`.
37
+ - `--output-dir`: Write report outputs to this directory (report.json, summary.txt, per-page JSONs, report UI).
38
+ - `--output`: Write a comprehensive JSON report to this file path.
39
+ - `--output-summary`: Write a human-readable summary report to this file path.
40
+ - `--save-html`: Save rendered HTML for each page to the output directory.
41
+ - `--checks`: Comma-separated list of page check names to run (others disabled).
42
+ - `--disable-checks`: Comma-separated list of page check names to skip.
43
+ - `--concurrency`: Number of pages to process concurrently, or `auto`. Default: 1.
44
+
45
+ Notes
46
+ - The page checks are modular. Add a new check under `worai/seocheck/checks` and register it in `worai/seocheck/checks/__init__.py`.
47
+ - The browser is Playwright (Chromium) to ensure JavaScript rendering.
48
+ - When using `--output-dir`, open `index.html` from that directory to view the report UI (or serve it with a web server).
49
+
50
+ Examples
51
+ - `worai seocheck https://example.com/sitemap.xml`
52
+ - `worai seocheck ./sitemap.xml`
53
+ - `worai seocheck /path/to/sitemap.xml.gz`
54
+ - `worai seocheck https://example.com/sitemap.xml --wait-until networkidle`
55
+ - `worai seocheck https://example.com/sitemap.xml --max-urls 25 --format json`
56
+ - `worai seocheck https://example.com/sitemap.xml --output ./report.json --output-summary ./report.txt`
57
+ - `worai seocheck https://example.com/sitemap.xml --output-dir ./seocheck-report --save-html`
58
+ - `worai seocheck https://example.com/sitemap.xml --checks status,page_meta,canonical`
59
+
60
+ Command Examples
61
+ - `worai google-search-console --site sc-domain:example.com --client-secrets ./client_secrets.json`
62
+ - `worai dedupe --dry-run`
63
+ - `worai canonicalize-duplicate-pages --input gsc_pages.csv --output canonical_targets.csv --kpi-window 28d --kpi-metric clicks`
64
+ - `worai delete-entities-from-csv ./entities.csv --batch-size 20`
65
+ - `worai find-faq-page-wrong-type ./data.ttl`
66
+ - `worai find-faq-page-wrong-type ./data.ttl --dry-run --replace-type`
67
+ - `worai find-faq-page-wrong-type ./data.ttl --patch --replace-type`
68
+ - `worai find-missing-names ./data.ttl`
69
+ - `worai find-url-by-type ./data.ttl schema:Service schema:Product`
70
+ - `worai link-groups ./links.csv --format turtle`
71
+ - `worai link-groups ./links.csv --apply --dry-run --concurrency 4`
72
+ - `worai patch ./data.ttl --dry-run --add-types`
73
+ - `worai patch ./data.jsonld --types-only --workers 4`
74
+ - `worai upload-entities-from-turtle ./entities --recursive --limit 50`
75
+
76
+ Client secrets format
77
+ The `--client-secrets` file is the OAuth2 client configuration downloaded from Google Cloud Console.
78
+ It looks like:
79
+
80
+ {
81
+ "installed": {
82
+ "client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
83
+ "project_id": "your-project-id",
84
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
85
+ "token_uri": "https://oauth2.googleapis.com/token",
86
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
87
+ "client_secret": "YOUR_CLIENT_SECRET",
88
+ "redirect_uris": [
89
+ "http://localhost"
90
+ ]
91
+ }
92
+ }
93
+
94
+ Setup steps
95
+ 1. Google Cloud Console -> APIs & Services -> OAuth consent screen:
96
+ - Configure a consent screen (External or Internal) and add yourself as a test user if required.
97
+ 2. APIs & Services -> Credentials -> Create Credentials -> OAuth client ID:
98
+ - Application type: Desktop app.
99
+ 3. Download the JSON and pass its path to `--client-secrets`.
100
+ 4. Run the command:
101
+ - `worai google-search-console --site sc-domain:example.com --client-secrets /path/to/client_secrets.json`
102
+ - If your OAuth client requires a fixed redirect URI (e.g. Web app), add `--port 8080` and register `http://localhost:8080`.
103
+ 5. The first run opens a browser for consent and writes a token file (default `gsc_token.json`).
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "worai"
3
+ version = "1.0.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "google-auth>=2.35.0",
9
+ "google-auth-oauthlib>=1.2.1",
10
+ "playwright>=1.48.0",
11
+ "rdflib>=7.5.0",
12
+ "requests>=2.32.5",
13
+ "tqdm>=4.67.1",
14
+ "typer>=0.12.5",
15
+ "wordlift-client>=1.133.0",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=8.3.4",
21
+ ]
22
+
23
+ [tool.pytest.ini_options]
24
+ pythonpath = ["."]
25
+
26
+ [project.scripts]
27
+ worai = "worai.cli:run"
28
+
29
+ [tool.setuptools]
30
+ package-dir = {"" = "."}
31
+
32
+ [tool.setuptools.packages.find]
33
+ include = ["worai*"]
34
+ exclude = ["specs*", "scripts*"]
worai-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from worai.core.canonicalize_duplicate_pages import load_gsc_csv, select_canonical
6
+
7
+
8
+ def test_load_gsc_csv(tmp_path: Path) -> None:
9
+ csv_path = tmp_path / "gsc.csv"
10
+ csv_path.write_text(
11
+ "page,clicks_28d,impressions_28d\n"
12
+ "https://example.com/a,10,100\n"
13
+ "https://example.com/b,5,50\n"
14
+ )
15
+
16
+ data = load_gsc_csv(str(csv_path), None)
17
+ assert data["https://example.com/a"]["clicks_28d"] == 10.0
18
+
19
+
20
+ def test_select_canonical() -> None:
21
+ urls = ["https://example.com/a", "https://example.com/b"]
22
+ kpis = {
23
+ "https://example.com/a": {"clicks_28d": 10.0},
24
+ "https://example.com/b": {"clicks_28d": 5.0},
25
+ }
26
+ canonical = select_canonical(urls, kpis, "clicks_28d")
27
+ assert canonical == "https://example.com/a"
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from worai.config import load_config
7
+
8
+
9
+ def test_load_config_profile(tmp_path: Path, monkeypatch) -> None:
10
+ config_path = tmp_path / "worai.toml"
11
+ config_path.write_text(
12
+ """
13
+ [defaults]
14
+ timeout = 10
15
+
16
+ [profile.dev]
17
+ timeout = 20
18
+ """
19
+ )
20
+
21
+ cfg = load_config(str(config_path), "dev", env={})
22
+ assert cfg.get("defaults.timeout") == 10
23
+ assert cfg.get("timeout") == 20
24
+
25
+
26
+ def test_env_config_override(tmp_path: Path, monkeypatch) -> None:
27
+ config_path = tmp_path / "worai.toml"
28
+ config_path.write_text("[defaults]\nlog_level = 'info'\n")
29
+
30
+ env = {"WORAI_CONFIG": str(config_path)}
31
+ cfg = load_config(None, None, env=env)
32
+ assert cfg.get("defaults.log_level") == "info"
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from worai.core.dedupe import group_by_url
4
+
5
+
6
+ def test_group_by_url() -> None:
7
+ entities = [
8
+ {"iri": "iri1", "url": "https://example.com/a"},
9
+ {"iri": "iri2", "url": "https://example.com/a"},
10
+ {"iri": "iri3", "url": "https://example.com/b"},
11
+ ]
12
+ grouped = group_by_url(entities)
13
+ assert grouped["https://example.com/a"] == ["iri1", "iri2"]
14
+ assert grouped["https://example.com/b"] == ["iri3"]
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from worai.core.find_missing_names import find_pages_without_name_or_headline
6
+
7
+
8
+ def test_find_pages_without_name_or_headline(tmp_path: Path) -> None:
9
+ ttl = """
10
+ @prefix schema: <http://schema.org/> .
11
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
12
+
13
+ <https://example.com/page1> rdf:type schema:CollectionPage ;
14
+ schema:url <https://example.com/page1> .
15
+
16
+ <https://example.com/page2> rdf:type schema:CollectionPage ;
17
+ schema:url <https://example.com/page2> ;
18
+ schema:name "Has name" .
19
+ """
20
+ ttl_path = tmp_path / "data.ttl"
21
+ ttl_path.write_text(ttl)
22
+
23
+ results = find_pages_without_name_or_headline(str(ttl_path))
24
+ assert results == ["https://example.com/page1"]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from worai.core.find_url_by_type import FindUrlByTypeOptions, find_urls, resolve_types
6
+
7
+
8
+ def test_resolve_types_filters_schema_prefix() -> None:
9
+ types = resolve_types(["schema:Service", "BadType"])
10
+ assert types == ["http://schema.org/Service"]
11
+
12
+
13
+ def test_find_urls(tmp_path: Path) -> None:
14
+ ttl = """
15
+ @prefix schema: <http://schema.org/> .
16
+
17
+ <https://example.com/a> a schema:Service ;
18
+ schema:url <https://example.com/a> .
19
+
20
+ <https://example.com/b> a schema:Product ;
21
+ schema:url <https://example.com/b> .
22
+ """
23
+ ttl_path = tmp_path / "data.ttl"
24
+ ttl_path.write_text(ttl)
25
+
26
+ options = FindUrlByTypeOptions(filename=str(ttl_path), types=["schema:Service"], show_id=False)
27
+ results = find_urls(options)
28
+ assert results == [("https://example.com/a", "https://example.com/a")]
@@ -0,0 +1,4 @@
1
+ """WORAi CLI package."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,79 @@
1
+ """WORAi root CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ import typer
8
+
9
+ from worai.config import load_config
10
+ from worai.errors import WoraError
11
+ from worai.logging import setup_logging
12
+
13
+ app = typer.Typer(add_completion=True, no_args_is_help=True)
14
+
15
+
16
+ @app.callback()
17
+ def main(
18
+ ctx: typer.Context,
19
+ config: str | None = typer.Option(None, "--config", help="Path to config TOML."),
20
+ profile: str | None = typer.Option(None, "--profile", help="Config profile name."),
21
+ log_level: str = typer.Option("info", "--log-level", help="Log level."),
22
+ log_format: str = typer.Option("text", "--log-format", help="Log format: text or json."),
23
+ quiet: bool = typer.Option(False, "--quiet", help="Only warnings and errors."),
24
+ ) -> None:
25
+ """WORAi command-line tools."""
26
+ setup_logging(level=log_level, fmt=log_format, quiet=quiet)
27
+ ctx.obj = {
28
+ "config": load_config(config, profile),
29
+ "log_level": log_level,
30
+ "log_format": log_format,
31
+ "quiet": quiet,
32
+ }
33
+
34
+
35
+ @app.command("version")
36
+ def version() -> None:
37
+ """Print version."""
38
+ from worai import __version__
39
+
40
+ typer.echo(__version__)
41
+
42
+
43
+ def _install_commands() -> None:
44
+ from worai.commands import (
45
+ canonicalize_duplicate_pages,
46
+ delete_entities_from_csv,
47
+ dedupe,
48
+ find_faq_page_wrong_type,
49
+ find_missing_names,
50
+ find_url_by_type,
51
+ google_search_console,
52
+ link_groups,
53
+ patch,
54
+ seocheck,
55
+ upload_entities_from_turtle,
56
+ )
57
+
58
+ app.add_typer(seocheck.app, name="seocheck")
59
+ app.add_typer(google_search_console.app, name="google-search-console")
60
+ app.add_typer(dedupe.app, name="dedupe")
61
+ app.add_typer(canonicalize_duplicate_pages.app, name="canonicalize-duplicate-pages")
62
+ app.add_typer(delete_entities_from_csv.app, name="delete-entities-from-csv")
63
+ app.add_typer(find_faq_page_wrong_type.app, name="find-faq-page-wrong-type")
64
+ app.add_typer(find_missing_names.app, name="find-missing-names")
65
+ app.add_typer(find_url_by_type.app, name="find-url-by-type")
66
+ app.add_typer(link_groups.app, name="link-groups")
67
+ app.add_typer(patch.app, name="patch")
68
+ app.add_typer(upload_entities_from_turtle.app, name="upload-entities-from-turtle")
69
+
70
+
71
+ _install_commands()
72
+
73
+
74
+ def run() -> None:
75
+ try:
76
+ app()
77
+ except WoraError as exc:
78
+ logging.getLogger("worai").error(str(exc))
79
+ raise SystemExit(exc.exit_code) from exc
@@ -0,0 +1 @@
1
+ """CLI subcommands."""
@@ -0,0 +1,39 @@
1
+ """CLI wrapper for canonicalize-duplicate-pages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from worai.core.canonicalize_duplicate_pages import CanonicalizeOptions, run as canonicalize_run
8
+ from worai.core.wordlift import DEFAULT_GRAPHQL_ENDPOINT, resolve_api_key
9
+ from worai.errors import UsageError
10
+
11
+ app = typer.Typer(add_completion=False, no_args_is_help=True)
12
+
13
+
14
+ @app.callback()
15
+ def run(
16
+ ctx: typer.Context,
17
+ input_csv: str = typer.Option(..., "--input", help="Input GSC CSV from worai google-search-console."),
18
+ output_csv: str = typer.Option("canonical_targets.csv", "--output", help="Output CSV path."),
19
+ url_regex: str | None = typer.Option(None, "--url-regex", help="Regex to filter URLs of interest."),
20
+ endpoint: str = typer.Option(DEFAULT_GRAPHQL_ENDPOINT, "--endpoint", help="WordLift GraphQL endpoint."),
21
+ batch_size: int = typer.Option(25, "--batch-size", help="Batch size for GraphQL queries."),
22
+ kpi_window: str = typer.Option("28d", "--kpi-window", help="KPI time window (7d, 28d, 3m)."),
23
+ kpi_metric: str = typer.Option("clicks", "--kpi-metric", help="KPI metric (clicks, impressions, ctr)."),
24
+ ) -> None:
25
+ api_key = resolve_api_key(ctx.obj.get("config") if ctx.obj else None)
26
+ if not api_key:
27
+ raise UsageError("WORDLIFT_KEY is required (or set wordlift.api_key in config).")
28
+
29
+ options = CanonicalizeOptions(
30
+ api_key=api_key,
31
+ input_csv=input_csv,
32
+ output_csv=output_csv,
33
+ url_regex=url_regex,
34
+ endpoint=endpoint,
35
+ batch_size=batch_size,
36
+ kpi_window=kpi_window,
37
+ kpi_metric=kpi_metric,
38
+ )
39
+ canonicalize_run(options)
@@ -0,0 +1,33 @@
1
+ """CLI wrapper for dedupe."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from worai.core.dedupe import DedupeOptions, run as dedupe_run
8
+ from worai.core.wordlift import resolve_api_key, DEFAULT_GRAPHQL_ENDPOINT
9
+ from worai.errors import UsageError
10
+
11
+ app = typer.Typer(add_completion=False, no_args_is_help=True)
12
+
13
+
14
+ @app.callback()
15
+ def run(
16
+ ctx: typer.Context,
17
+ endpoint: str = typer.Option(DEFAULT_GRAPHQL_ENDPOINT, "--endpoint", help="GraphQL endpoint URL."),
18
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show delete calls without executing them."),
19
+ rate_delay: float = typer.Option(0.0, "--rate-delay", help="Seconds to sleep between delete calls."),
20
+ auto: bool = typer.Option(False, "--auto", help="Automatically keep the last IRI in each group."),
21
+ ) -> None:
22
+ api_key = resolve_api_key(ctx.obj.get("config") if ctx.obj else None)
23
+ if not api_key:
24
+ raise UsageError("WORDLIFT_KEY is required (or set wordlift.api_key in config).")
25
+
26
+ options = DedupeOptions(
27
+ api_key=api_key,
28
+ endpoint=endpoint,
29
+ dry_run=dry_run,
30
+ rate_delay=rate_delay,
31
+ auto=auto,
32
+ )
33
+ dedupe_run(options)
@@ -0,0 +1,25 @@
1
+ """CLI wrapper for delete-entities-from-csv."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from worai.core.delete_entities_from_csv import DeleteEntitiesOptions, delete_entities
8
+ from worai.core.wordlift import resolve_api_key
9
+ from worai.errors import UsageError
10
+
11
+ app = typer.Typer(add_completion=False, no_args_is_help=True)
12
+
13
+
14
+ @app.callback()
15
+ def run(
16
+ ctx: typer.Context,
17
+ csv_file: str = typer.Argument(..., help="CSV file with IRIs (first column)."),
18
+ batch_size: int = typer.Option(10, "--batch-size", help="Number of IRIs to delete per batch."),
19
+ ) -> None:
20
+ api_key = resolve_api_key(ctx.obj.get("config") if ctx.obj else None)
21
+ if not api_key:
22
+ raise UsageError("WORDLIFT_KEY is required (or set wordlift.api_key in config).")
23
+
24
+ options = DeleteEntitiesOptions(api_key=api_key, csv_file=csv_file, batch_size=batch_size)
25
+ delete_entities(options)
@@ -0,0 +1,41 @@
1
+ """CLI wrapper for find-faq-page-wrong-type."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import typer
7
+
8
+ from worai.core.find_faq_page_wrong_type import FaqFixOptions, run as faq_run
9
+ from worai.core.wordlift import resolve_api_key
10
+ from worai.errors import UsageError
11
+
12
+ app = typer.Typer(add_completion=False, no_args_is_help=True)
13
+
14
+
15
+ @app.callback()
16
+ def run(
17
+ ctx: typer.Context,
18
+ file_path: str = typer.Argument(..., help="Path to Turtle RDF file."),
19
+ patch: bool = typer.Option(False, "--patch", help="Execute live API patch calls."),
20
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be patched."),
21
+ replace_type: bool = typer.Option(False, "--replace-type", help="Remove existing types before adding FAQPage."),
22
+ ) -> None:
23
+ mode = "find"
24
+ if patch:
25
+ mode = "patch"
26
+ elif dry_run:
27
+ mode = "dry-run"
28
+
29
+ api_key = None
30
+ if mode == "patch":
31
+ api_key = resolve_api_key(ctx.obj.get("config") if ctx.obj else None) or os.environ.get("WORDLIFT_API_KEY")
32
+ if not api_key:
33
+ raise UsageError("WORDLIFT_KEY is required for --patch (or set wordlift.api_key in config).")
34
+
35
+ options = FaqFixOptions(
36
+ file_path=file_path,
37
+ mode=mode,
38
+ replace_type=replace_type,
39
+ api_key=api_key,
40
+ )
41
+ faq_run(options)
@@ -0,0 +1,35 @@
1
+ """CLI wrapper for find-missing-names."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ import typer
8
+
9
+ from worai.core.find_missing_names import run as find_run
10
+
11
+ app = typer.Typer(add_completion=False, no_args_is_help=True)
12
+
13
+
14
+ @app.callback()
15
+ def run(
16
+ file_path: str = typer.Argument(..., help="Path to RDF file."),
17
+ ) -> None:
18
+ try:
19
+ results = find_run(file_path)
20
+ except FileNotFoundError:
21
+ typer.echo(f"Error: The file '{file_path}' was not found.", err=True)
22
+ raise typer.Exit(code=1)
23
+ except Exception as exc:
24
+ typer.echo(f"An error occurred while parsing the file: {exc}", err=True)
25
+ raise typer.Exit(code=1)
26
+
27
+ if results:
28
+ typer.echo(
29
+ f"Found URLs for {len(results)} schema:CollectionPage entities without a 'schema:name' or 'schema:headline':"
30
+ )
31
+ typer.echo("-" * 80)
32
+ for url in results:
33
+ typer.echo(url)
34
+ else:
35
+ typer.echo("No schema:CollectionPage entities are missing both 'schema:name' and 'schema:headline'.")