monopigi 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,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: monopigi
3
+ Version: 0.1.0
4
+ Summary: Python SDK and CLI for the Monopigi Greek Government Data API
5
+ Keywords: greek,government,data,api,procurement,open-data
6
+ Author: Monopigi
7
+ Author-email: Monopigi <info@monopigi.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Requires-Dist: httpx>=0.28
15
+ Requires-Dist: pydantic>=2.0
16
+ Requires-Dist: typer>=0.15
17
+ Requires-Dist: rich>=13.0
18
+ Requires-Dist: textual>=3.0
19
+ Requires-Dist: polars>=1.0 ; extra == 'all'
20
+ Requires-Dist: polars>=1.0 ; extra == 'df'
21
+ Requires-Python: >=3.12
22
+ Project-URL: Homepage, https://monopigi.com
23
+ Provides-Extra: all
24
+ Provides-Extra: df
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "monopigi"
3
+ version = "0.1.0"
4
+ description = "Python SDK and CLI for the Monopigi Greek Government Data API"
5
+ requires-python = ">=3.12"
6
+ license = "MIT"
7
+ authors = [{name = "Monopigi", email = "info@monopigi.com"}]
8
+ keywords = ["greek", "government", "data", "api", "procurement", "open-data"]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Topic :: Software Development :: Libraries",
15
+ ]
16
+ dependencies = [
17
+ "httpx>=0.28",
18
+ "pydantic>=2.0",
19
+ "typer>=0.15",
20
+ "rich>=13.0",
21
+ "textual>=3.0",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ df = ["polars>=1.0"]
26
+ all = ["polars>=1.0"]
27
+
28
+ [project.scripts]
29
+ monopigi = "monopigi.cli:app"
30
+
31
+ [project.urls]
32
+ Homepage = "https://monopigi.com"
33
+
34
+ [build-system]
35
+ requires = ["uv_build>=0.10.9,<0.11.0"]
36
+ build-backend = "uv_build"
@@ -0,0 +1,23 @@
1
+ """Monopigi -- Python SDK for the Greek Government Data API."""
2
+
3
+ from monopigi.client import AsyncMonopigiClient, MonopigiClient
4
+ from monopigi.exceptions import AuthError, MonopigiError, NotFoundError, RateLimitError, TierError
5
+ from monopigi.models import Country, Document, OutputFormat, QuotaInfo, Source, SourceStatus, Tier
6
+
7
+ __version__ = "0.1.0"
8
+ __all__ = [
9
+ "AsyncMonopigiClient",
10
+ "AuthError",
11
+ "Country",
12
+ "Document",
13
+ "MonopigiClient",
14
+ "MonopigiError",
15
+ "NotFoundError",
16
+ "OutputFormat",
17
+ "QuotaInfo",
18
+ "RateLimitError",
19
+ "Source",
20
+ "SourceStatus",
21
+ "Tier",
22
+ "TierError",
23
+ ]
@@ -0,0 +1,74 @@
1
+ """Interactive document browser using Textual TUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ HAS_TEXTUAL = False
6
+
7
+ try:
8
+ from textual.app import App, ComposeResult
9
+ from textual.binding import Binding
10
+ from textual.widgets import DataTable, Footer, Header, Input
11
+
12
+ HAS_TEXTUAL = True
13
+ except ImportError:
14
+ pass
15
+
16
+
17
+ def check_textual() -> None:
18
+ """Raise a helpful error if textual is not installed."""
19
+ if not HAS_TEXTUAL:
20
+ raise ImportError("Interactive browse requires 'textual'. Install with: pip install monopigi-sdk[fuzzy]")
21
+
22
+
23
+ if HAS_TEXTUAL:
24
+ from typing import ClassVar
25
+
26
+ class DocumentBrowser(App): # type: ignore[misc]
27
+ """Interactive TUI for browsing Monopigi documents."""
28
+
29
+ TITLE = "Monopigi Document Browser"
30
+ BINDINGS: ClassVar[list[Binding]] = [
31
+ Binding("q", "quit", "Quit"),
32
+ Binding("escape", "quit", "Quit"),
33
+ ]
34
+
35
+ def __init__(self, documents: list[dict], source: str = "") -> None:
36
+ super().__init__()
37
+ self._documents = documents
38
+ self._source = source
39
+
40
+ def compose(self) -> ComposeResult:
41
+ yield Header()
42
+ yield Input(placeholder="Type to filter...", id="filter")
43
+ yield DataTable()
44
+ yield Footer()
45
+
46
+ def on_mount(self) -> None:
47
+ table = self.query_one(DataTable)
48
+ table.add_columns("Source", "Title", "Date", "Score")
49
+ self._populate_table(table, "")
50
+
51
+ def on_input_changed(self, event: Input.Changed) -> None:
52
+ table = self.query_one(DataTable)
53
+ table.clear()
54
+ self._populate_table(table, event.value.lower())
55
+
56
+ def _populate_table(self, table: DataTable, filter_text: str) -> None:
57
+ for doc in self._documents:
58
+ title = doc.get("title") or "\u2014"
59
+ source_lower = (doc.get("source") or "").lower()
60
+ if filter_text and filter_text not in title.lower() and filter_text not in source_lower:
61
+ continue
62
+ table.add_row(
63
+ doc.get("source", "\u2014"),
64
+ title[:80],
65
+ doc.get("published_at") or "\u2014",
66
+ f"{doc.get('quality_score', 0):.2f}" if doc.get("quality_score") else "\u2014",
67
+ )
68
+
69
+
70
+ def browse_documents(documents: list[dict], source: str = "") -> None:
71
+ """Launch the interactive document browser."""
72
+ check_textual()
73
+ app = DocumentBrowser(documents, source)
74
+ app.run()
@@ -0,0 +1,41 @@
1
+ """Simple disk-based response cache with TTL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import time
8
+ from pathlib import Path
9
+
10
+ DEFAULT_CACHE_DIR = Path.home() / ".monopigi" / "cache"
11
+
12
+
13
+ class DiskCache:
14
+ """Cache API responses to disk as JSON files with a time-to-live."""
15
+
16
+ def __init__(self, ttl: int, cache_dir: Path = DEFAULT_CACHE_DIR) -> None:
17
+ self.ttl = ttl
18
+ self.cache_dir = cache_dir
19
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
20
+
21
+ def _key(self, method: str, url: str, params: str) -> str:
22
+ raw = f"{method}:{url}:{params}"
23
+ return hashlib.sha256(raw.encode()).hexdigest()
24
+
25
+ def get(self, method: str, url: str, params: str) -> str | None:
26
+ """Return cached body if present and not expired, else None."""
27
+ key = self._key(method, url, params)
28
+ path = self.cache_dir / f"{key}.json"
29
+ if not path.exists():
30
+ return None
31
+ data = json.loads(path.read_text())
32
+ if time.time() - data["ts"] > self.ttl:
33
+ path.unlink()
34
+ return None
35
+ return data["body"]
36
+
37
+ def set(self, method: str, url: str, params: str, body: str) -> None:
38
+ """Store a response body in the cache."""
39
+ key = self._key(method, url, params)
40
+ path = self.cache_dir / f"{key}.json"
41
+ path.write_text(json.dumps({"ts": time.time(), "body": body}))