publicsgdata 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.
Files changed (30) hide show
  1. publicsgdata-0.1.0/.gitignore +21 -0
  2. publicsgdata-0.1.0/CHANGELOG.md +19 -0
  3. publicsgdata-0.1.0/LICENSE +21 -0
  4. publicsgdata-0.1.0/PKG-INFO +130 -0
  5. publicsgdata-0.1.0/README.md +95 -0
  6. publicsgdata-0.1.0/pyproject.toml +104 -0
  7. publicsgdata-0.1.0/src/publicsgdata/__init__.py +23 -0
  8. publicsgdata-0.1.0/src/publicsgdata/_base_client.py +138 -0
  9. publicsgdata-0.1.0/src/publicsgdata/_constants.py +17 -0
  10. publicsgdata-0.1.0/src/publicsgdata/_exceptions.py +38 -0
  11. publicsgdata-0.1.0/src/publicsgdata/_pagination.py +150 -0
  12. publicsgdata-0.1.0/src/publicsgdata/datagovsg/__init__.py +6 -0
  13. publicsgdata-0.1.0/src/publicsgdata/datagovsg/_request.py +65 -0
  14. publicsgdata-0.1.0/src/publicsgdata/datagovsg/async_client.py +73 -0
  15. publicsgdata-0.1.0/src/publicsgdata/datagovsg/client.py +73 -0
  16. publicsgdata-0.1.0/src/publicsgdata/datagovsg/models/__init__.py +29 -0
  17. publicsgdata-0.1.0/src/publicsgdata/datagovsg/models/common.py +141 -0
  18. publicsgdata-0.1.0/src/publicsgdata/datagovsg/resources/collections.py +43 -0
  19. publicsgdata-0.1.0/src/publicsgdata/datagovsg/resources/datasets.py +248 -0
  20. publicsgdata-0.1.0/src/publicsgdata/datagovsg/resources/realtime/__init__.py +11 -0
  21. publicsgdata-0.1.0/src/publicsgdata/datagovsg/resources/realtime/pm25.py +58 -0
  22. publicsgdata-0.1.0/tests/conftest.py +51 -0
  23. publicsgdata-0.1.0/tests/fixtures/collection_metadata.json +13 -0
  24. publicsgdata-0.1.0/tests/fixtures/collections_list.json +18 -0
  25. publicsgdata-0.1.0/tests/fixtures/dataset_rows.json +24 -0
  26. publicsgdata-0.1.0/tests/fixtures/datastore_search.json +14 -0
  27. publicsgdata-0.1.0/tests/fixtures/pm25.json +14 -0
  28. publicsgdata-0.1.0/tests/test_async_datagovsg.py +15 -0
  29. publicsgdata-0.1.0/tests/test_datagovsg.py +37 -0
  30. publicsgdata-0.1.0/tests/test_integration_datagovsg.py +74 -0
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ .venv/
7
+ venv/
8
+ env/
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .pytest_cache/
15
+ .coverage
16
+ coverage.xml
17
+ htmlcov/
18
+ *.log
19
+ .DS_Store
20
+ .idea/
21
+ .vscode/
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-06-09
8
+
9
+ ### Added
10
+
11
+ - `DataGovSGClient` and `AsyncDataGovSGClient`: sync and async, with optional custom `httpx` clients
12
+ - `collections.list()` and `collections.get_metadata()`
13
+ - `datasets.list()`, `get_metadata()`, `list_rows()`, `iter_rows()`, and CKAN `search()`
14
+ - `realtime.pm25.get()` for PM2.5 readings
15
+ - Pydantic v2 response models
16
+ - Optional `DATA_GOV_SG_API_KEY` auth via `x-api-key` header
17
+ - CI, release-please, and PyPI publish workflows
18
+
19
+ [0.1.0]: https://github.com/publicsgdata/publicsgdata/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Harry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: publicsgdata
3
+ Version: 0.1.0
4
+ Summary: Python client for Singapore government open data (data.gov.sg, LTA, OneMap)
5
+ Project-URL: Homepage, https://github.com/publicsgdata/publicsgdata
6
+ Project-URL: Repository, https://github.com/publicsgdata/publicsgdata
7
+ Project-URL: Documentation, https://github.com/publicsgdata/publicsgdata#readme
8
+ Project-URL: Issues, https://github.com/publicsgdata/publicsgdata/issues
9
+ Author: publicsgdata contributors
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,data.gov.sg,open-data,sdk,singapore
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: httpx<1,>=0.27.0
25
+ Requires-Dist: pydantic<3,>=2.0
26
+ Requires-Dist: typing-extensions<5,>=4.8
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.0; extra == 'dev'
29
+ Requires-Dist: mypy>=1.8; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
31
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.8; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # publicsgdata
37
+
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
39
+
40
+ Python client for Singapore government open data: data.gov.sg today, LTA and OneMap later.
41
+
42
+ ## Install
43
+
44
+ Requires [uv](https://docs.astral.sh/uv/getting-started/installation/).
45
+
46
+ ```bash
47
+ uv pip install publicsgdata
48
+ ```
49
+
50
+ ## Quickstart
51
+
52
+ ```python
53
+ from publicsgdata import DataGovSGClient
54
+
55
+ with DataGovSGClient() as client: # optional: api_key="..." or DATA_GOV_SG_API_KEY
56
+ catalog = client.collections.list()
57
+ print(f"{len(catalog.collections)} collections")
58
+ print(catalog.collections[0].name)
59
+
60
+ # HDB resale prices (swap in any dataset ID)
61
+ rows = client.datasets.list_rows("d_8b84c4ee58e3cfc0ece0d773c8ca6abc", limit=10)
62
+ for row in rows.rows:
63
+ print(row.model_dump())
64
+
65
+ pm25 = client.realtime.pm25.get()
66
+ print(pm25.items[0].readings)
67
+ ```
68
+
69
+ ### Async
70
+
71
+ ```python
72
+ from publicsgdata import AsyncDataGovSGClient
73
+
74
+ async with AsyncDataGovSGClient() as client:
75
+ rows = await client.datasets.list_rows("d_8b84c4ee58e3cfc0ece0d773c8ca6abc", limit=5)
76
+ print(len(rows.rows))
77
+ ```
78
+
79
+ ### Custom HTTP client
80
+
81
+ Pass your own `httpx` client if you need custom timeouts, proxies, etc.
82
+
83
+ ```python
84
+ import httpx
85
+ from publicsgdata import DataGovSGClient
86
+
87
+ with httpx.Client(timeout=30.0) as http:
88
+ client = DataGovSGClient(http_client=http)
89
+ print(len(client.collections.list().collections))
90
+ ```
91
+
92
+ ## Authentication
93
+
94
+ You can call the API without a key while experimenting. For regular use, get a key from [data.gov.sg](https://data.gov.sg/) and set:
95
+
96
+ ```bash
97
+ export DATA_GOV_SG_API_KEY="your-key"
98
+ ```
99
+
100
+ ## Environment variables
101
+
102
+ | Variable | Required | Description |
103
+ |---|---|---|
104
+ | `DATA_GOV_SG_API_KEY` | No | data.gov.sg API key (`x-api-key` header) |
105
+
106
+ ## Development
107
+
108
+ You'll need [uv](https://docs.astral.sh/uv/getting-started/installation/).
109
+
110
+ ```bash
111
+ ./scripts/dev_setup.sh # creates .venv from uv.lock
112
+ ./scripts/format.sh
113
+ ./scripts/validate.sh
114
+ ./scripts/test.sh # unit tests, runs in CI
115
+ ./scripts/test_integration.sh # hits the real API, local only
116
+ ```
117
+
118
+ Or run things directly: `uv run pytest`, `uv run ruff check .`, etc.
119
+
120
+ See [CONTRIBUTING.md](CONTRIBUTING.md) if you're opening a PR.
121
+
122
+ ## Roadmap
123
+
124
+ - **v0.1.0**: `DataGovSGClient`
125
+ - **v0.2.0**: `LTAClient` (LTA DataMall)
126
+ - **v0.3.0**: `OneMapClient`
127
+
128
+ ## License
129
+
130
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,95 @@
1
+ # publicsgdata
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+
5
+ Python client for Singapore government open data: data.gov.sg today, LTA and OneMap later.
6
+
7
+ ## Install
8
+
9
+ Requires [uv](https://docs.astral.sh/uv/getting-started/installation/).
10
+
11
+ ```bash
12
+ uv pip install publicsgdata
13
+ ```
14
+
15
+ ## Quickstart
16
+
17
+ ```python
18
+ from publicsgdata import DataGovSGClient
19
+
20
+ with DataGovSGClient() as client: # optional: api_key="..." or DATA_GOV_SG_API_KEY
21
+ catalog = client.collections.list()
22
+ print(f"{len(catalog.collections)} collections")
23
+ print(catalog.collections[0].name)
24
+
25
+ # HDB resale prices (swap in any dataset ID)
26
+ rows = client.datasets.list_rows("d_8b84c4ee58e3cfc0ece0d773c8ca6abc", limit=10)
27
+ for row in rows.rows:
28
+ print(row.model_dump())
29
+
30
+ pm25 = client.realtime.pm25.get()
31
+ print(pm25.items[0].readings)
32
+ ```
33
+
34
+ ### Async
35
+
36
+ ```python
37
+ from publicsgdata import AsyncDataGovSGClient
38
+
39
+ async with AsyncDataGovSGClient() as client:
40
+ rows = await client.datasets.list_rows("d_8b84c4ee58e3cfc0ece0d773c8ca6abc", limit=5)
41
+ print(len(rows.rows))
42
+ ```
43
+
44
+ ### Custom HTTP client
45
+
46
+ Pass your own `httpx` client if you need custom timeouts, proxies, etc.
47
+
48
+ ```python
49
+ import httpx
50
+ from publicsgdata import DataGovSGClient
51
+
52
+ with httpx.Client(timeout=30.0) as http:
53
+ client = DataGovSGClient(http_client=http)
54
+ print(len(client.collections.list().collections))
55
+ ```
56
+
57
+ ## Authentication
58
+
59
+ You can call the API without a key while experimenting. For regular use, get a key from [data.gov.sg](https://data.gov.sg/) and set:
60
+
61
+ ```bash
62
+ export DATA_GOV_SG_API_KEY="your-key"
63
+ ```
64
+
65
+ ## Environment variables
66
+
67
+ | Variable | Required | Description |
68
+ |---|---|---|
69
+ | `DATA_GOV_SG_API_KEY` | No | data.gov.sg API key (`x-api-key` header) |
70
+
71
+ ## Development
72
+
73
+ You'll need [uv](https://docs.astral.sh/uv/getting-started/installation/).
74
+
75
+ ```bash
76
+ ./scripts/dev_setup.sh # creates .venv from uv.lock
77
+ ./scripts/format.sh
78
+ ./scripts/validate.sh
79
+ ./scripts/test.sh # unit tests, runs in CI
80
+ ./scripts/test_integration.sh # hits the real API, local only
81
+ ```
82
+
83
+ Or run things directly: `uv run pytest`, `uv run ruff check .`, etc.
84
+
85
+ See [CONTRIBUTING.md](CONTRIBUTING.md) if you're opening a PR.
86
+
87
+ ## Roadmap
88
+
89
+ - **v0.1.0**: `DataGovSGClient`
90
+ - **v0.2.0**: `LTAClient` (LTA DataMall)
91
+ - **v0.3.0**: `OneMapClient`
92
+
93
+ ## License
94
+
95
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,104 @@
1
+ [project]
2
+ name = "publicsgdata"
3
+ version = "0.1.0"
4
+ description = "Python client for Singapore government open data (data.gov.sg, LTA, OneMap)"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.10"
8
+ authors = [{ name = "publicsgdata contributors" }]
9
+ keywords = ["singapore", "open-data", "data.gov.sg", "api", "sdk"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Typing :: Typed",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+ dependencies = [
23
+ "httpx>=0.27.0,<1",
24
+ "pydantic>=2.0,<3",
25
+ "typing-extensions>=4.8,<5",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "build>=1.0",
31
+ "mypy>=1.8",
32
+ "pytest>=8.0",
33
+ "pytest-asyncio>=0.24",
34
+ "pytest-cov>=5.0",
35
+ "ruff>=0.8",
36
+ ]
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "build>=1.0",
41
+ "mypy>=1.8",
42
+ "pytest>=8.0",
43
+ "pytest-asyncio>=0.24",
44
+ "pytest-cov>=5.0",
45
+ "ruff>=0.8",
46
+ ]
47
+
48
+ [tool.uv]
49
+ default-groups = ["dev"]
50
+
51
+ [project.urls]
52
+ Homepage = "https://github.com/publicsgdata/publicsgdata"
53
+ Repository = "https://github.com/publicsgdata/publicsgdata"
54
+ Documentation = "https://github.com/publicsgdata/publicsgdata#readme"
55
+ Issues = "https://github.com/publicsgdata/publicsgdata/issues"
56
+
57
+ [build-system]
58
+ requires = ["hatchling>=1.26"]
59
+ build-backend = "hatchling.build"
60
+
61
+ [tool.hatch.build.targets.wheel]
62
+ packages = ["src/publicsgdata"]
63
+
64
+ [tool.hatch.build.targets.sdist]
65
+ include = [
66
+ "/README.md",
67
+ "/LICENSE",
68
+ "/CHANGELOG.md",
69
+ "/src/publicsgdata",
70
+ "/tests",
71
+ ]
72
+
73
+ [tool.pytest.ini_options]
74
+ asyncio_mode = "auto"
75
+ asyncio_default_fixture_loop_scope = "function"
76
+ testpaths = ["tests"]
77
+ markers = [
78
+ "integration: live data.gov.sg API tests (local only; use ./scripts/test_integration.sh)",
79
+ ]
80
+
81
+ [tool.ruff]
82
+ line-length = 100
83
+ target-version = "py310"
84
+ src = ["src", "tests"]
85
+
86
+ [tool.ruff.lint]
87
+ select = ["E", "F", "I", "UP", "B", "SIM"]
88
+
89
+ [tool.mypy]
90
+ python_version = "3.10"
91
+ strict = true
92
+ warn_return_any = true
93
+ warn_unused_configs = true
94
+ packages = ["publicsgdata"]
95
+ mypy_path = "src"
96
+ files = ["src", "tests"]
97
+
98
+ [[tool.mypy.overrides]]
99
+ module = ["conftest", "test_datagovsg", "test_async_datagovsg", "test_integration_datagovsg"]
100
+ disallow_untyped_defs = false
101
+
102
+ [tool.coverage.run]
103
+ source = ["publicsgdata"]
104
+ branch = true
@@ -0,0 +1,23 @@
1
+ """publicsgdata: Python client for Singapore government public data."""
2
+
3
+ from publicsgdata._exceptions import (
4
+ APIError,
5
+ AuthenticationError,
6
+ NotFoundError,
7
+ PublicSGDataError,
8
+ RateLimitError,
9
+ )
10
+ from publicsgdata.datagovsg.async_client import AsyncDataGovSGClient
11
+ from publicsgdata.datagovsg.client import DataGovSGClient
12
+
13
+ __all__ = [
14
+ "APIError",
15
+ "AsyncDataGovSGClient",
16
+ "AuthenticationError",
17
+ "DataGovSGClient",
18
+ "NotFoundError",
19
+ "PublicSGDataError",
20
+ "RateLimitError",
21
+ ]
22
+
23
+ __version__ = "0.1.0"
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any, Literal, cast
5
+
6
+ import httpx
7
+
8
+ from publicsgdata._constants import DEFAULT_TIMEOUT, HEADER_API_KEY
9
+ from publicsgdata._exceptions import (
10
+ APIError,
11
+ AuthenticationError,
12
+ NotFoundError,
13
+ RateLimitError,
14
+ )
15
+
16
+
17
+ class BaseHTTPClient:
18
+ """Shared HTTP helpers for sync and async clients."""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ api_key: str | None = None,
24
+ timeout: float = DEFAULT_TIMEOUT,
25
+ max_retries: int = 0,
26
+ ) -> None:
27
+ self._api_key = api_key
28
+ self._timeout = timeout
29
+ self._max_retries = max_retries
30
+ self._owns_client = False
31
+
32
+ def _auth_headers(self) -> dict[str, str]:
33
+ if self._api_key:
34
+ return {HEADER_API_KEY: self._api_key}
35
+ return {}
36
+
37
+ def _merge_headers(self, headers: Mapping[str, str] | None = None) -> dict[str, str]:
38
+ merged = dict(self._auth_headers())
39
+ if headers:
40
+ merged.update(headers)
41
+ return merged
42
+
43
+ def _parse_json(self, response: httpx.Response) -> Any:
44
+ if not response.content:
45
+ return None
46
+ return response.json()
47
+
48
+ def _raise_for_response(
49
+ self,
50
+ response: httpx.Response,
51
+ *,
52
+ payload: Any | None = None,
53
+ ) -> None:
54
+ if response.is_success:
55
+ if isinstance(payload, dict):
56
+ code = payload.get("code")
57
+ if code not in (None, 0) and payload.get("success") is not True:
58
+ self._raise_api_payload(response, payload)
59
+ return
60
+
61
+ data = payload if payload is not None else self._parse_json(response)
62
+ if response.status_code == 429:
63
+ raise RateLimitError(
64
+ _error_message(data, response),
65
+ status_code=429,
66
+ code=_error_code(data),
67
+ name=_error_name(data),
68
+ body=data,
69
+ )
70
+ if response.status_code in (401, 403):
71
+ raise AuthenticationError(
72
+ _error_message(data, response),
73
+ status_code=response.status_code,
74
+ code=_error_code(data),
75
+ name=_error_name(data),
76
+ body=data,
77
+ )
78
+ if response.status_code == 404:
79
+ raise NotFoundError(
80
+ _error_message(data, response),
81
+ status_code=404,
82
+ code=_error_code(data),
83
+ name=_error_name(data),
84
+ body=data,
85
+ )
86
+ raise APIError(
87
+ _error_message(data, response),
88
+ status_code=response.status_code,
89
+ code=_error_code(data),
90
+ name=_error_name(data),
91
+ body=data,
92
+ )
93
+
94
+ def _raise_api_payload(self, response: httpx.Response, payload: dict[str, Any]) -> None:
95
+ message = _error_message(payload, response)
96
+ code = _error_code(payload)
97
+ name = _error_name(payload)
98
+ if response.status_code == 429 or code == 429:
99
+ raise RateLimitError(message, status_code=429, code=code, name=name, body=payload)
100
+ if name and "NOT_FOUND" in name.upper():
101
+ raise NotFoundError(message, status_code=404, code=code, name=name, body=payload)
102
+ raise APIError(
103
+ message,
104
+ status_code=response.status_code,
105
+ code=code,
106
+ name=name,
107
+ body=payload,
108
+ )
109
+
110
+
111
+ def _error_message(data: Any, response: httpx.Response) -> str:
112
+ if isinstance(data, dict):
113
+ for key in ("errorMsg", "message", "error"):
114
+ value = data.get(key)
115
+ if isinstance(value, str) and value:
116
+ return value
117
+ if isinstance(value, dict):
118
+ return str(value)
119
+ if data.get("success") is False and isinstance(data.get("error"), dict):
120
+ return str(data["error"])
121
+ return f"HTTP {response.status_code} error for {response.request.url}"
122
+
123
+
124
+ def _error_code(data: Any) -> int | str | None:
125
+ if isinstance(data, dict) and "code" in data:
126
+ return cast(int | str | None, data.get("code"))
127
+ return None
128
+
129
+
130
+ def _error_name(data: Any) -> str | None:
131
+ if isinstance(data, dict) and isinstance(data.get("name"), str):
132
+ return cast(str, data["name"])
133
+ return None
134
+
135
+
136
+ SyncHTTPClient = httpx.Client
137
+ AsyncHTTPClient = httpx.AsyncClient
138
+ HTTPMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"]
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ ENV_API_KEY = "DATA_GOV_SG_API_KEY"
6
+ HEADER_API_KEY = "x-api-key"
7
+
8
+ DEFAULT_TIMEOUT = 30.0
9
+
10
+ # data.gov.sg hosts
11
+ CATALOG_BASE_URL = "https://api-production.data.gov.sg/v2/public/api"
12
+ CKAN_BASE_URL = "https://data.gov.sg"
13
+ REALTIME_BASE_URL = "https://api-open.data.gov.sg/v2/real-time/api"
14
+
15
+
16
+ def default_api_key() -> str | None:
17
+ return os.environ.get(ENV_API_KEY)
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class PublicSGDataError(Exception):
7
+ """Base exception for publicsgdata."""
8
+
9
+
10
+ class APIError(PublicSGDataError):
11
+ """Raised when the API returns an error response."""
12
+
13
+ def __init__(
14
+ self,
15
+ message: str,
16
+ *,
17
+ status_code: int | None = None,
18
+ code: int | str | None = None,
19
+ name: str | None = None,
20
+ body: Any | None = None,
21
+ ) -> None:
22
+ super().__init__(message)
23
+ self.status_code = status_code
24
+ self.code = code
25
+ self.name = name
26
+ self.body = body
27
+
28
+
29
+ class RateLimitError(APIError):
30
+ """Raised when rate limits are exceeded (HTTP 429)."""
31
+
32
+
33
+ class AuthenticationError(APIError):
34
+ """Raised when authentication fails (HTTP 401/403)."""
35
+
36
+
37
+ class NotFoundError(APIError):
38
+ """Raised when a resource is not found (HTTP 404)."""