onspy 0.1.0__py3-none-any.whl
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.
- onspy/__init__.py +56 -0
- onspy/browse.py +85 -0
- onspy/client.py +209 -0
- onspy/code_lists.py +300 -0
- onspy/datasets.py +366 -0
- onspy/exceptions.py +54 -0
- onspy/get.py +284 -0
- onspy/search.py +78 -0
- onspy/utils.py +222 -0
- onspy-0.1.0.dist-info/METADATA +411 -0
- onspy-0.1.0.dist-info/RECORD +14 -0
- onspy-0.1.0.dist-info/WHEEL +5 -0
- onspy-0.1.0.dist-info/licenses/LICENSE +217 -0
- onspy-0.1.0.dist-info/top_level.txt +1 -0
onspy/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
onspy: Python client for the Office of National Statistics (ONS) API
|
|
3
|
+
|
|
4
|
+
This package provides client functions for accessing the Office of National Statistics API
|
|
5
|
+
at https://api.beta.ons.gov.uk/v1.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
# Configure logging
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
logger.setLevel(logging.INFO)
|
|
14
|
+
|
|
15
|
+
# Add console handler if not already added
|
|
16
|
+
if not logger.handlers:
|
|
17
|
+
console_handler = logging.StreamHandler()
|
|
18
|
+
console_handler.setLevel(logging.INFO)
|
|
19
|
+
formatter = logging.Formatter(
|
|
20
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
21
|
+
)
|
|
22
|
+
console_handler.setFormatter(formatter)
|
|
23
|
+
logger.addHandler(console_handler)
|
|
24
|
+
|
|
25
|
+
# Enable debug logging if ONS_DEBUG is set
|
|
26
|
+
if os.environ.get("ONS_DEBUG", "").lower() in ("1", "true", "yes"):
|
|
27
|
+
logger.setLevel(logging.DEBUG)
|
|
28
|
+
for handler in logger.handlers:
|
|
29
|
+
handler.setLevel(logging.DEBUG)
|
|
30
|
+
logger.debug("Debug logging enabled for onspy")
|
|
31
|
+
|
|
32
|
+
from .datasets import (
|
|
33
|
+
ons_datasets,
|
|
34
|
+
ons_ids,
|
|
35
|
+
ons_desc,
|
|
36
|
+
ons_editions,
|
|
37
|
+
ons_latest_href,
|
|
38
|
+
ons_latest_version,
|
|
39
|
+
ons_latest_edition,
|
|
40
|
+
ons_find_latest_version_across_editions,
|
|
41
|
+
)
|
|
42
|
+
from .get import ons_get, ons_get_obs, ons_dim, ons_dim_opts, ons_meta
|
|
43
|
+
from .code_lists import (
|
|
44
|
+
ons_codelists,
|
|
45
|
+
ons_codelist,
|
|
46
|
+
ons_codelist_editions,
|
|
47
|
+
ons_codelist_edition,
|
|
48
|
+
ons_codes,
|
|
49
|
+
ons_code,
|
|
50
|
+
ons_code_dataset,
|
|
51
|
+
)
|
|
52
|
+
from .search import ons_search
|
|
53
|
+
from .browse import ons_browse, ons_browse_qmi
|
|
54
|
+
|
|
55
|
+
__version__ = "0.1.0"
|
|
56
|
+
__author__ = "Joe Wait"
|
onspy/browse.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Browser functionality for ONS.
|
|
3
|
+
|
|
4
|
+
This module provides functions to quickly open ONS webpages in a browser.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import webbrowser
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from .datasets import ons_datasets, assert_valid_id, id_number
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ons_browse() -> str:
|
|
14
|
+
"""Quickly browse to ONS' developer webpage.
|
|
15
|
+
|
|
16
|
+
This function opens the ONS developer webpage in a browser.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The URL of the webpage
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
>>> import onspy
|
|
23
|
+
>>> onspy.ons_browse()
|
|
24
|
+
"""
|
|
25
|
+
url = "https://developer.ons.gov.uk/"
|
|
26
|
+
_open_url(url)
|
|
27
|
+
return url
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ons_browse_qmi(id: str = None) -> Optional[str]:
|
|
31
|
+
"""Quickly browse to dataset's Quality and Methodology Information (QMI).
|
|
32
|
+
|
|
33
|
+
This function opens the QMI webpage for a dataset in a browser.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
id: Dataset ID
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The URL of the webpage, or None if the dataset is not found
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
>>> import onspy
|
|
43
|
+
>>> onspy.ons_browse_qmi("cpih01")
|
|
44
|
+
"""
|
|
45
|
+
datasets = ons_datasets()
|
|
46
|
+
if datasets is None:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
if not assert_valid_id(id, datasets):
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
idx = id_number(id, datasets)
|
|
53
|
+
|
|
54
|
+
# Handle nested dictionary
|
|
55
|
+
try:
|
|
56
|
+
if hasattr(datasets.iloc[idx], "qmi") and hasattr(
|
|
57
|
+
datasets.iloc[idx].qmi, "href"
|
|
58
|
+
):
|
|
59
|
+
url = datasets.iloc[idx].qmi.href
|
|
60
|
+
elif isinstance(datasets.iloc[idx].get("qmi", {}), dict):
|
|
61
|
+
url = datasets.iloc[idx]["qmi"].get("href", None)
|
|
62
|
+
else:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
if url:
|
|
66
|
+
_open_url(url)
|
|
67
|
+
return url
|
|
68
|
+
return None
|
|
69
|
+
except (AttributeError, KeyError, IndexError):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _open_url(url: str, open_browser: bool = True) -> str:
|
|
74
|
+
"""Open a URL in the default browser.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
url: The URL to open
|
|
78
|
+
open_browser: Whether to actually open the browser (default: True)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The URL
|
|
82
|
+
"""
|
|
83
|
+
if open_browser:
|
|
84
|
+
webbrowser.open(url)
|
|
85
|
+
return url
|
onspy/client.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ONS API Client module.
|
|
3
|
+
|
|
4
|
+
This module provides a centralized client class for making requests to the ONS API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import requests
|
|
9
|
+
import json
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ONSClient:
|
|
16
|
+
"""Client for interacting with the Office of National Statistics API."""
|
|
17
|
+
|
|
18
|
+
# API Constants
|
|
19
|
+
EMPTY = ""
|
|
20
|
+
ENDPOINT = "https://api.beta.ons.gov.uk/v1"
|
|
21
|
+
# Identify your bot
|
|
22
|
+
USER_AGENT = (
|
|
23
|
+
"onspy/0.1.0 (MyOrganisation contact@myorg.com +http://www.myorg.com/bot.html)"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def __init__(self, endpoint: Optional[str] = None):
|
|
27
|
+
"""Initialize the ONS client.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
endpoint: Custom API endpoint URL (optional)
|
|
31
|
+
"""
|
|
32
|
+
self.endpoint = endpoint or self.ENDPOINT
|
|
33
|
+
self._session = requests.Session()
|
|
34
|
+
self._session.headers.update(self._get_browser_headers())
|
|
35
|
+
|
|
36
|
+
def _get_browser_headers(self) -> Dict[str, str]:
|
|
37
|
+
"""Get browser-like headers to help with HTTP requests.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Dictionary of headers that mimic a browser
|
|
41
|
+
"""
|
|
42
|
+
return {
|
|
43
|
+
"User-Agent": self.USER_AGENT,
|
|
44
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
45
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
46
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
47
|
+
"Connection": "keep-alive",
|
|
48
|
+
"Upgrade-Insecure-Requests": "1",
|
|
49
|
+
"Pragma": "no-cache",
|
|
50
|
+
"Cache-Control": "no-cache",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def has_internet(self) -> bool:
|
|
54
|
+
"""Check if internet connection is available.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
bool: True if internet connection is available, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
# Try to connect to a widely available service
|
|
61
|
+
self._session.get("https://www.google.com", timeout=5)
|
|
62
|
+
return True
|
|
63
|
+
except requests.ConnectionError:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def build_url(self, path_segments: Dict[str, Optional[str]]) -> str:
|
|
67
|
+
"""Build a request URL from path segments.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
path_segments: Dictionary mapping path segment names to values
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Full request URL
|
|
74
|
+
"""
|
|
75
|
+
# Build path from segments
|
|
76
|
+
path_parts = []
|
|
77
|
+
for key, value in path_segments.items():
|
|
78
|
+
if value is None:
|
|
79
|
+
continue
|
|
80
|
+
elif value == self.EMPTY:
|
|
81
|
+
path_parts.append(key)
|
|
82
|
+
else:
|
|
83
|
+
path_parts.append(f"{key}/{value}")
|
|
84
|
+
|
|
85
|
+
path = "/".join(path_parts)
|
|
86
|
+
url = f"{self.endpoint}/{path}"
|
|
87
|
+
|
|
88
|
+
logger.debug(f"Built URL: {url}")
|
|
89
|
+
return url
|
|
90
|
+
|
|
91
|
+
def make_request(
|
|
92
|
+
self,
|
|
93
|
+
url: str,
|
|
94
|
+
limit: Optional[int] = None,
|
|
95
|
+
offset: Optional[int] = None,
|
|
96
|
+
**kwargs,
|
|
97
|
+
) -> Optional[requests.Response]:
|
|
98
|
+
"""Make HTTP request to the ONS API.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
url: Request URL
|
|
102
|
+
limit: Number of records to return (optional)
|
|
103
|
+
offset: Position in the dataset to start from (optional)
|
|
104
|
+
**kwargs: Additional arguments to pass to requests.get
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Response object if successful, None otherwise
|
|
108
|
+
"""
|
|
109
|
+
logger.debug(f"Making request to URL: {url}")
|
|
110
|
+
if limit is not None:
|
|
111
|
+
logger.debug(f"With limit: {limit}")
|
|
112
|
+
if offset is not None:
|
|
113
|
+
logger.debug(f"With offset: {offset}")
|
|
114
|
+
|
|
115
|
+
if not self.has_internet():
|
|
116
|
+
logger.error("Unable to connect: No internet connection available")
|
|
117
|
+
print(
|
|
118
|
+
"Unable to connect: Please ensure you have an active internet connection or access through a secure connection."
|
|
119
|
+
)
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# Prepare parameters
|
|
124
|
+
params = {}
|
|
125
|
+
if limit is not None:
|
|
126
|
+
params["limit"] = limit
|
|
127
|
+
if offset is not None:
|
|
128
|
+
params["offset"] = offset
|
|
129
|
+
|
|
130
|
+
logger.debug(f"Request params: {params}")
|
|
131
|
+
|
|
132
|
+
# Try up to 3 times with exponential backoff
|
|
133
|
+
for attempt in range(3):
|
|
134
|
+
try:
|
|
135
|
+
logger.debug(f"Attempt {attempt+1}/3")
|
|
136
|
+
|
|
137
|
+
response = self._session.get(
|
|
138
|
+
url, params=params, timeout=30, **kwargs
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
logger.debug(f"Response status code: {response.status_code}")
|
|
142
|
+
|
|
143
|
+
response.raise_for_status() # Raise exception for HTTP errors
|
|
144
|
+
|
|
145
|
+
if response.status_code == 200:
|
|
146
|
+
logger.debug(
|
|
147
|
+
f"Request successful. Content length: {len(response.content)}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return response
|
|
151
|
+
|
|
152
|
+
except (
|
|
153
|
+
requests.exceptions.RequestException,
|
|
154
|
+
requests.exceptions.HTTPError,
|
|
155
|
+
) as e:
|
|
156
|
+
logger.warning(f"Request failed: {e}")
|
|
157
|
+
|
|
158
|
+
if attempt == 2: # Last attempt
|
|
159
|
+
logger.error(f"Request failed after 3 attempts: {e}")
|
|
160
|
+
print(f"Request failed: {e}")
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
logger.debug(f"Retrying...")
|
|
164
|
+
continue # Try again
|
|
165
|
+
|
|
166
|
+
except Exception as err:
|
|
167
|
+
logger.error(f"Unexpected error during request: {err}", exc_info=True)
|
|
168
|
+
print(f"Error: {err}")
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
def process_response(self, response: requests.Response) -> Dict[str, Any]:
|
|
172
|
+
"""Process HTTP response and convert to JSON.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
response: HTTP response object
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
JSON content as dictionary
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
json_data = response.json()
|
|
182
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
183
|
+
logger.debug(f"Response JSON keys: {list(json_data.keys())}")
|
|
184
|
+
if "items" in json_data:
|
|
185
|
+
logger.debug(f"Number of items: {len(json_data['items'])}")
|
|
186
|
+
if len(json_data["items"]) > 0:
|
|
187
|
+
logger.debug(
|
|
188
|
+
f"First item keys: {list(json_data['items'][0].keys())}"
|
|
189
|
+
)
|
|
190
|
+
return json_data
|
|
191
|
+
except json.JSONDecodeError:
|
|
192
|
+
logger.error("Error decoding JSON response", exc_info=True)
|
|
193
|
+
print("Error decoding JSON response")
|
|
194
|
+
return {}
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def get_instance(cls) -> "ONSClient":
|
|
198
|
+
"""Get a singleton instance of the ONS client.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
ONSClient instance
|
|
202
|
+
"""
|
|
203
|
+
if not hasattr(cls, "_instance"):
|
|
204
|
+
cls._instance = cls()
|
|
205
|
+
return cls._instance
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# Create a default client instance
|
|
209
|
+
default_client = ONSClient.get_instance()
|
onspy/code_lists.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Explore codes and lists.
|
|
3
|
+
|
|
4
|
+
This module provides functions to get details about codes and code lists stored by ONS.
|
|
5
|
+
Codes are used to provide a common definition when presenting statistics with related categories.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional, List, Dict, Any
|
|
9
|
+
|
|
10
|
+
from .utils import (
|
|
11
|
+
build_base_request,
|
|
12
|
+
make_request,
|
|
13
|
+
process_response,
|
|
14
|
+
EMPTY,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ons_codelists() -> Optional[List[str]]:
|
|
19
|
+
"""Get a list of all available code lists.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of code list IDs, or None if the request fails
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
>>> import onspy
|
|
26
|
+
>>> onspy.ons_codelists()
|
|
27
|
+
"""
|
|
28
|
+
req = build_base_request(**{"code-lists": EMPTY})
|
|
29
|
+
res = make_request(req, limit=80)
|
|
30
|
+
if res is None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
raw = process_response(res)
|
|
34
|
+
|
|
35
|
+
# Extract IDs from items
|
|
36
|
+
try:
|
|
37
|
+
return [item["links"]["self"]["id"] for item in raw.get("items", [])]
|
|
38
|
+
except (KeyError, TypeError):
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def assert_valid_codeid(id: str) -> bool:
|
|
43
|
+
"""Check if a code list ID is valid.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
id: Code list ID
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True if valid, raises ValueError otherwise
|
|
50
|
+
"""
|
|
51
|
+
if id is None:
|
|
52
|
+
raise ValueError("You must specify a 'code_id', see ons_codelists()")
|
|
53
|
+
|
|
54
|
+
ids = ons_codelists()
|
|
55
|
+
if ids is None:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
if id not in ids:
|
|
59
|
+
raise ValueError(f"Invalid code_id '{id}'. See ons_codelists() for valid IDs.")
|
|
60
|
+
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ons_codelist(code_id: str = None) -> Optional[Dict[str, Any]]:
|
|
65
|
+
"""Get details for a specific code list.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
code_id: Code list ID
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dictionary with code list details, or None if the request fails
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> import onspy
|
|
75
|
+
>>> onspy.ons_codelist(code_id="quarter")
|
|
76
|
+
"""
|
|
77
|
+
if not assert_valid_codeid(code_id):
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
req = build_base_request(**{"code-lists": code_id})
|
|
81
|
+
res = make_request(req)
|
|
82
|
+
if res is None:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
return process_response(res)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def ons_codelist_editions(code_id: str = None) -> Optional[List[Dict[str, Any]]]:
|
|
89
|
+
"""Get editions for a code list.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
code_id: Code list ID
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
List of editions, or None if the request fails
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
>>> import onspy
|
|
99
|
+
>>> onspy.ons_codelist_editions(code_id="quarter")
|
|
100
|
+
"""
|
|
101
|
+
if not assert_valid_codeid(code_id):
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
req = build_base_request(**{"code-lists": code_id, "editions": EMPTY})
|
|
105
|
+
res = make_request(req)
|
|
106
|
+
if res is None:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
raw = process_response(res)
|
|
110
|
+
return raw.get("items", [])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def assert_valid_edition(code_id: str, edition: str) -> bool:
|
|
114
|
+
"""Check if an edition is valid for a code list.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
code_id: Code list ID
|
|
118
|
+
edition: Edition name
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True if valid, raises ValueError otherwise
|
|
122
|
+
"""
|
|
123
|
+
if edition is None:
|
|
124
|
+
raise ValueError("You must specify an 'edition', see ons_codelist_editions()")
|
|
125
|
+
|
|
126
|
+
editions = ons_codelist_editions(code_id)
|
|
127
|
+
if editions is None:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
edition_names = [e.get("edition", "") for e in editions]
|
|
131
|
+
if edition not in edition_names:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Invalid edition '{edition}'. Valid editions are: {', '.join(edition_names)}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def ons_codelist_edition(
|
|
140
|
+
code_id: str = None, edition: str = None
|
|
141
|
+
) -> Optional[Dict[str, Any]]:
|
|
142
|
+
"""Get details for a specific edition of a code list.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
code_id: Code list ID
|
|
146
|
+
edition: Edition name
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dictionary with edition details, or None if the request fails
|
|
150
|
+
|
|
151
|
+
Examples:
|
|
152
|
+
>>> import onspy
|
|
153
|
+
>>> onspy.ons_codelist_edition(code_id="quarter", edition="one-off")
|
|
154
|
+
"""
|
|
155
|
+
if not assert_valid_codeid(code_id):
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
if not assert_valid_edition(code_id, edition):
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
req = build_base_request(**{"code-lists": code_id, "editions": edition})
|
|
162
|
+
res = make_request(req)
|
|
163
|
+
if res is None:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
return process_response(res)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def ons_codes(
|
|
170
|
+
code_id: str = None, edition: str = None
|
|
171
|
+
) -> Optional[List[Dict[str, Any]]]:
|
|
172
|
+
"""Get codes for a specific edition of a code list.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
code_id: Code list ID
|
|
176
|
+
edition: Edition name
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of codes, or None if the request fails
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
>>> import onspy
|
|
183
|
+
>>> onspy.ons_codes(code_id="quarter", edition="one-off")
|
|
184
|
+
"""
|
|
185
|
+
if not assert_valid_codeid(code_id):
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
if not assert_valid_edition(code_id, edition):
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
req = build_base_request(
|
|
192
|
+
**{"code-lists": code_id, "editions": edition, "codes": EMPTY}
|
|
193
|
+
)
|
|
194
|
+
res = make_request(req)
|
|
195
|
+
if res is None:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
raw = process_response(res)
|
|
199
|
+
return raw.get("items", [])
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def assert_valid_code(code_id: str, edition: str, code: str) -> bool:
|
|
203
|
+
"""Check if a code is valid for an edition of a code list.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
code_id: Code list ID
|
|
207
|
+
edition: Edition name
|
|
208
|
+
code: Code value
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
True if valid, raises ValueError otherwise
|
|
212
|
+
"""
|
|
213
|
+
if code is None:
|
|
214
|
+
raise ValueError("You must specify a 'code', see ons_codes()")
|
|
215
|
+
|
|
216
|
+
codes = ons_codes(code_id, edition)
|
|
217
|
+
if codes is None:
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
code_values = [c.get("code", "") for c in codes]
|
|
221
|
+
if code not in code_values:
|
|
222
|
+
raise ValueError(
|
|
223
|
+
f"Invalid code '{code}'. Valid codes are: {', '.join(code_values)}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def ons_code(
|
|
230
|
+
code_id: str = None, edition: str = None, code: str = None
|
|
231
|
+
) -> Optional[Dict[str, Any]]:
|
|
232
|
+
"""Get details for a specific code.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
code_id: Code list ID
|
|
236
|
+
edition: Edition name
|
|
237
|
+
code: Code value
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Dictionary with code details, or None if the request fails
|
|
241
|
+
|
|
242
|
+
Examples:
|
|
243
|
+
>>> import onspy
|
|
244
|
+
>>> onspy.ons_code(code_id="quarter", edition="one-off", code="q2")
|
|
245
|
+
"""
|
|
246
|
+
if not assert_valid_codeid(code_id):
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
if not assert_valid_edition(code_id, edition):
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
if not assert_valid_code(code_id, edition, code):
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
req = build_base_request(
|
|
256
|
+
**{"code-lists": code_id, "editions": edition, "codes": code}
|
|
257
|
+
)
|
|
258
|
+
res = make_request(req)
|
|
259
|
+
if res is None:
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
return process_response(res)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def ons_code_dataset(
|
|
266
|
+
code_id: str = None, edition: str = None, code: str = None
|
|
267
|
+
) -> Optional[List[Dict[str, Any]]]:
|
|
268
|
+
"""Get datasets that use a specific code.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
code_id: Code list ID
|
|
272
|
+
edition: Edition name
|
|
273
|
+
code: Code value
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of datasets, or None if the request fails
|
|
277
|
+
|
|
278
|
+
Examples:
|
|
279
|
+
>>> import onspy
|
|
280
|
+
>>> onspy.ons_code_dataset(code_id="quarter", edition="one-off", code="q2")
|
|
281
|
+
"""
|
|
282
|
+
if not assert_valid_codeid(code_id):
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
if not assert_valid_edition(code_id, edition):
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
if not assert_valid_code(code_id, edition, code):
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
req = build_base_request(
|
|
292
|
+
**{"code-lists": code_id, "editions": edition, "codes": code, "datasets": EMPTY}
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
res = make_request(req)
|
|
296
|
+
if res is None:
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
raw = process_response(res)
|
|
300
|
+
return raw.get("items", [])
|