jstdata 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.
Potentially problematic release.
This version of jstdata might be problematic. Click here for more details.
- jstdata-0.1.0/LICENSE +21 -0
- jstdata-0.1.0/PKG-INFO +60 -0
- jstdata-0.1.0/README.md +33 -0
- jstdata-0.1.0/jstdata/__init__.py +1 -0
- jstdata-0.1.0/jstdata/cli.py +213 -0
- jstdata-0.1.0/jstdata/client.py +258 -0
- jstdata-0.1.0/jstdata/utils.py +58 -0
- jstdata-0.1.0/pyproject.toml +41 -0
jstdata-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mahfuj Ahbab
|
|
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.
|
jstdata-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jstdata
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python interface to the Jefferson Street REST API.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: api,cli,data,jefferson street
|
|
8
|
+
Author: Mahfuj Ahbab
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Dist: click (>=8.2.1,<9.0.0)
|
|
21
|
+
Requires-Dist: pandas (>=2.3.1,<3.0.0)
|
|
22
|
+
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
23
|
+
Project-URL: Homepage, https://github.com/mahfuj/python_api
|
|
24
|
+
Project-URL: Repository, https://github.com/mahfuj/python_api
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# jstdata
|
|
28
|
+
|
|
29
|
+
A Python interface to the Jefferson Street REST API.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install jstdata
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
Set your API key as an environment variable:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export JEFFERSON_STREET_API_KEY="your_api_key_here"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then you can use the CLI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
jst --help
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or use it as a Python library:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from jstdata.client import JeffersonStreetClient
|
|
55
|
+
|
|
56
|
+
client = JeffersonStreetClient("your_api_key_here")
|
|
57
|
+
metrics = client.get_metrics()
|
|
58
|
+
print(metrics)
|
|
59
|
+
```
|
|
60
|
+
|
jstdata-0.1.0/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# jstdata
|
|
2
|
+
|
|
3
|
+
A Python interface to the Jefferson Street REST API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install jstdata
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Set your API key as an environment variable:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export JEFFERSON_STREET_API_KEY="your_api_key_here"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then you can use the CLI:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
jst --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or use it as a Python library:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from jstdata.client import JeffersonStreetClient
|
|
29
|
+
|
|
30
|
+
client = JeffersonStreetClient("your_api_key_here")
|
|
31
|
+
metrics = client.get_metrics()
|
|
32
|
+
print(metrics)
|
|
33
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .client import JeffersonStreetClient
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from .client import ApiKeyNotSetError, JeffersonStreetClient
|
|
7
|
+
from .utils import common_params, format_and_print
|
|
8
|
+
|
|
9
|
+
api_key = os.getenv("JEFFERSON_STREET_API_KEY")
|
|
10
|
+
if api_key is None:
|
|
11
|
+
raise click.Abort(
|
|
12
|
+
"Please set the JEFFERSON_STREET_API_KEY environment variable to a valid API key."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
client = JeffersonStreetClient(api_key)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
def cli():
|
|
20
|
+
"""
|
|
21
|
+
Jefferson Street CLI.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cli.group()
|
|
26
|
+
def metric():
|
|
27
|
+
"""
|
|
28
|
+
Commands for interacting with metrics.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@cli.group()
|
|
33
|
+
def entity():
|
|
34
|
+
"""
|
|
35
|
+
Commands for interacting with entities.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cli.group()
|
|
40
|
+
def query():
|
|
41
|
+
"""
|
|
42
|
+
Commands for querying data.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@entity.command("groups")
|
|
47
|
+
@click.option(
|
|
48
|
+
"--format",
|
|
49
|
+
default="pretty",
|
|
50
|
+
help="Output format. Valid formats are: json, csv, pretty.",
|
|
51
|
+
)
|
|
52
|
+
def list_entity_groups(format):
|
|
53
|
+
"""
|
|
54
|
+
List all available entity groups.
|
|
55
|
+
"""
|
|
56
|
+
entity_groups = client.get_entity_groups()
|
|
57
|
+
format_and_print(entity_groups, format)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@entity.command("values")
|
|
61
|
+
@click.argument("entity_group", required=True)
|
|
62
|
+
@common_params
|
|
63
|
+
def list_entities(entity_group, limit, format, offset):
|
|
64
|
+
"""
|
|
65
|
+
List entities within a specified entity group.
|
|
66
|
+
"""
|
|
67
|
+
results = client.get_entities(entity_group, offset, limit)
|
|
68
|
+
format_and_print(results, format)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@entity.command("metrics")
|
|
72
|
+
@click.argument("entity", required=True)
|
|
73
|
+
@common_params
|
|
74
|
+
def get_entity_links(entity, limit, format, offset):
|
|
75
|
+
"""
|
|
76
|
+
Retrieve metrics associated with a specific entity.
|
|
77
|
+
"""
|
|
78
|
+
results = client.get_entity_metrics(entity, limit, offset)
|
|
79
|
+
format_and_print(results, format)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@metric.command("ls")
|
|
83
|
+
@common_params
|
|
84
|
+
@click.option(
|
|
85
|
+
"--sort_order", default="desc", help="Sort order (asc or desc, default: desc)"
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--order_by",
|
|
89
|
+
default="last_updated",
|
|
90
|
+
help="Column to order by (default: last_updated)",
|
|
91
|
+
)
|
|
92
|
+
@click.option("--expanded", is_flag=True, default=False, help="Return expanded metrics")
|
|
93
|
+
def list_metrics(limit, offset, sort_order, order_by, format, expanded):
|
|
94
|
+
"""
|
|
95
|
+
List all available metrics.
|
|
96
|
+
"""
|
|
97
|
+
metrics = client.get_metrics(None, limit, offset, order_by, sort_order)
|
|
98
|
+
if expanded:
|
|
99
|
+
format_and_print(metrics, format)
|
|
100
|
+
else:
|
|
101
|
+
condensed_metrics = [{"slug": m["slug"]} for m in metrics]
|
|
102
|
+
format_and_print(condensed_metrics, format)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@metric.command("show")
|
|
106
|
+
@click.argument("metric", required=True)
|
|
107
|
+
@click.option(
|
|
108
|
+
"--format",
|
|
109
|
+
default="pretty",
|
|
110
|
+
help="Output format. Valid formats are: json, csv, pretty.",
|
|
111
|
+
)
|
|
112
|
+
def show_metric(metric, format):
|
|
113
|
+
"""
|
|
114
|
+
Display details for a specific metric.
|
|
115
|
+
"""
|
|
116
|
+
metric = client.get_metrics(metric)
|
|
117
|
+
format_and_print(metric, format)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@metric.command("entities")
|
|
121
|
+
@click.argument("metric", required=True)
|
|
122
|
+
@click.option(
|
|
123
|
+
"--format",
|
|
124
|
+
default="pretty",
|
|
125
|
+
help="Output format. Valid formats are: json, csv, pretty.",
|
|
126
|
+
)
|
|
127
|
+
def show_metric_dimensions(metric, format):
|
|
128
|
+
"""
|
|
129
|
+
Display dimensions for a specific metric.
|
|
130
|
+
"""
|
|
131
|
+
dimensions = client.get_metric_dimensions(metric)
|
|
132
|
+
format_and_print(dimensions, format)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@query.command("metric")
|
|
136
|
+
@common_params
|
|
137
|
+
@click.argument("metric", required=True, nargs=1)
|
|
138
|
+
@click.option(
|
|
139
|
+
"--sort_order", default="desc", help="Sort order (asc or desc, default: desc)"
|
|
140
|
+
)
|
|
141
|
+
@click.option("--order_by", default="id", help="Column to order by (default: id)")
|
|
142
|
+
@click.option(
|
|
143
|
+
"--start_date",
|
|
144
|
+
default=None,
|
|
145
|
+
help="Start period. Valid formats: YYYY-MM-DD, YYYY-MM, YYYY, unix timestamp",
|
|
146
|
+
)
|
|
147
|
+
@click.option(
|
|
148
|
+
"--end_date",
|
|
149
|
+
default=None,
|
|
150
|
+
help="End period. Valid formats: YYYY-MM-DD, YYYY-MM, YYYY, unix timestamp",
|
|
151
|
+
)
|
|
152
|
+
def get_observations_by_metric(
|
|
153
|
+
metric, sort_order, order_by, start_date, end_date, limit, offset, format
|
|
154
|
+
):
|
|
155
|
+
"""
|
|
156
|
+
Retrieve observations for a specific metric.
|
|
157
|
+
"""
|
|
158
|
+
observations = client.query("metric", metric, start_date, end_date, limit, offset)
|
|
159
|
+
format_and_print(observations, format)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@query.command("entity")
|
|
163
|
+
@click.argument("entity", required=True, nargs=1)
|
|
164
|
+
@click.option(
|
|
165
|
+
"--sort_order", default="desc", help="Sort order (asc or desc, default: desc)"
|
|
166
|
+
)
|
|
167
|
+
@click.option("--order_by", default="id", help="Column to order by (default: id)")
|
|
168
|
+
@click.option(
|
|
169
|
+
"--start_date",
|
|
170
|
+
default=None,
|
|
171
|
+
help="Start period. Valid formats: YYYY-MM-DD, YYYY-MM, YYYY, unix timestamp",
|
|
172
|
+
)
|
|
173
|
+
@click.option(
|
|
174
|
+
"--end_date",
|
|
175
|
+
default=None,
|
|
176
|
+
help="End period. Valid formats: YYYY-MM-DD, YYYY-MM, YYYY, unix timestamp",
|
|
177
|
+
)
|
|
178
|
+
@common_params
|
|
179
|
+
def get_observations_by_entity(
|
|
180
|
+
entity, sort_order, order_by, start_date, end_date, limit, offset, format
|
|
181
|
+
):
|
|
182
|
+
"""
|
|
183
|
+
Retrieve observations for a specific entity.
|
|
184
|
+
"""
|
|
185
|
+
observations = client.query("entity", entity, start_date, end_date, limit, offset)
|
|
186
|
+
format_and_print(observations, format)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@entity.command("search")
|
|
190
|
+
@click.option(
|
|
191
|
+
"--limit", default=3, help="Maximum number of records to return (default: 10000)"
|
|
192
|
+
)
|
|
193
|
+
@click.option("--offset", default=0, help="Number of records to skip (default: 0)")
|
|
194
|
+
@click.option(
|
|
195
|
+
"--format",
|
|
196
|
+
default="pretty",
|
|
197
|
+
help="Output format. Valid formats are: json, csv, pretty.",
|
|
198
|
+
)
|
|
199
|
+
@click.argument("query", required=True, nargs=1)
|
|
200
|
+
def search_for_entity(query, limit, offset, format):
|
|
201
|
+
"""
|
|
202
|
+
Search for entities.
|
|
203
|
+
"""
|
|
204
|
+
results = client.search_for_entity(query, limit, offset)
|
|
205
|
+
format_and_print(results, format)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
try:
|
|
210
|
+
cli()
|
|
211
|
+
except ApiKeyNotSetError as e:
|
|
212
|
+
click.echo(f"Error: {e}", err=True)
|
|
213
|
+
sys.exit(1)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
import requests
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
|
|
6
|
+
EntityType = namedtuple("EntityType", ["name", "slug", "classification"])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ApiKeyNotSetError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InvalidApiKeyError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InvalidInputError(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JeffersonStreetClient:
|
|
22
|
+
# For testing purposes
|
|
23
|
+
base_url = os.getenv("JEFFERSON_STREET_SERVER") or "https://api.jeffersonst.io"
|
|
24
|
+
|
|
25
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
26
|
+
"""
|
|
27
|
+
Initializes the JeffersonStreetClient with an API key.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
api_key: The API key for authenticating with the Jefferson Street REST API.
|
|
31
|
+
"""
|
|
32
|
+
if api_key is None:
|
|
33
|
+
raise ApiKeyNotSetError("API key is not set")
|
|
34
|
+
self._api_key = api_key
|
|
35
|
+
self.session = requests.Session()
|
|
36
|
+
self.session.params = {"api-key": api_key}
|
|
37
|
+
|
|
38
|
+
self._api_key_is_valid = False
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def api_key(self):
|
|
42
|
+
"""
|
|
43
|
+
Returns the API key used by the client.
|
|
44
|
+
"""
|
|
45
|
+
return self._api_key
|
|
46
|
+
|
|
47
|
+
def _validate_api_key(self, api_key) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Validates the provided API key by making a heartbeat request to the API.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
api_key: The API key to validate.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ApiKeyNotSetError: If the API key is None.
|
|
56
|
+
InvalidApiKeyError: If the API key is invalid.
|
|
57
|
+
"""
|
|
58
|
+
if api_key is None:
|
|
59
|
+
raise ApiKeyNotSetError("API key is not set")
|
|
60
|
+
|
|
61
|
+
url = f"{self.base_url}/heartbeat"
|
|
62
|
+
heartbeat = self.session.get(url).json()
|
|
63
|
+
if heartbeat["status"] != "ok":
|
|
64
|
+
raise InvalidApiKeyError("Invalid API key")
|
|
65
|
+
self._api_key_is_valid = True
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
def _make_request(
|
|
69
|
+
self, endpoint: str, params: Optional[Dict[str, Any]] = None
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
"""
|
|
72
|
+
Makes a GET request to the specified API endpoint.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
endpoint: The API endpoint to call.
|
|
76
|
+
params: A dictionary of query parameters to send with the request.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A dictionary containing the JSON response from the API.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ApiKeyNotSetError: If the API key is not set.
|
|
83
|
+
InvalidApiKeyError: If the API key is invalid.
|
|
84
|
+
requests.exceptions.RequestException: For network-related errors or unsuccessful HTTP responses.
|
|
85
|
+
"""
|
|
86
|
+
if not self._api_key_is_valid:
|
|
87
|
+
self._validate_api_key(self.api_key)
|
|
88
|
+
|
|
89
|
+
url = f"{self.base_url}/{endpoint}"
|
|
90
|
+
response = self.session.get(url, params=params)
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
return response.json()
|
|
93
|
+
|
|
94
|
+
def get_metrics(
|
|
95
|
+
self,
|
|
96
|
+
metric: Optional[str] = None,
|
|
97
|
+
limit: int = 100,
|
|
98
|
+
offset: int = 0,
|
|
99
|
+
order_by: str = "last_updated",
|
|
100
|
+
sort_order: str = "desc",
|
|
101
|
+
) -> Dict[str, Any]:
|
|
102
|
+
"""Get available metrics.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
metric: The metric's slug. If empty, all metrics are returned
|
|
106
|
+
limit: Maximum number of records to return (default: 10000)
|
|
107
|
+
offset: Number of records to skip (default: 0)
|
|
108
|
+
order_by: Column to order by (default: "last_updated")
|
|
109
|
+
sort_order: Sort order ("asc" or "desc", default: "desc")
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
MetricResponse containing list of available metrics
|
|
113
|
+
"""
|
|
114
|
+
response = self._make_request(
|
|
115
|
+
"metric",
|
|
116
|
+
{
|
|
117
|
+
"metric": metric,
|
|
118
|
+
"limit": limit,
|
|
119
|
+
"offset": offset,
|
|
120
|
+
"order_by": order_by,
|
|
121
|
+
"sort_order": sort_order,
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
return response["records"]
|
|
125
|
+
|
|
126
|
+
def get_metric_dimensions(self, metric: str) -> Dict[str, Any]:
|
|
127
|
+
try:
|
|
128
|
+
response = self._make_request(f"metric/{metric}/entities")
|
|
129
|
+
except requests.exceptions.HTTPError as e:
|
|
130
|
+
raise InvalidInputError(f"Invalid input: {e}")
|
|
131
|
+
return response["records"]
|
|
132
|
+
|
|
133
|
+
def get_entity_groups(self) -> Dict[str, Any]:
|
|
134
|
+
"""Get available entity types.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dictionary containing list of available entity types
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
response = self._make_request("entity/groups")
|
|
141
|
+
except requests.exceptions.HTTPError as e:
|
|
142
|
+
raise InvalidInputError(f"Invalid input: {e}")
|
|
143
|
+
return response["records"]
|
|
144
|
+
|
|
145
|
+
def get_entities(
|
|
146
|
+
self,
|
|
147
|
+
entity_group: Optional[str] = None,
|
|
148
|
+
offset: int = 0,
|
|
149
|
+
limit: int = 10000,
|
|
150
|
+
sort_order: str = "asc",
|
|
151
|
+
) -> Dict[str, Any]:
|
|
152
|
+
"""Get available entities.
|
|
153
|
+
Args:
|
|
154
|
+
offset: Number of records to skip (default: 0)
|
|
155
|
+
limit: Maximum number of records to return (default: 10000)
|
|
156
|
+
sort_order: Sort order ("asc" or "desc", default: "asc")
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Dictionary containing list of available entities
|
|
160
|
+
"""
|
|
161
|
+
response = self._make_request(
|
|
162
|
+
f"entity/{entity_group}",
|
|
163
|
+
{"offset": offset, "limit": limit, "sort_order": sort_order},
|
|
164
|
+
)
|
|
165
|
+
return response["records"]
|
|
166
|
+
|
|
167
|
+
def get_entity_metrics(
|
|
168
|
+
self,
|
|
169
|
+
entity: Optional[str] = None,
|
|
170
|
+
limit: int = 10000,
|
|
171
|
+
offset: int = 0,
|
|
172
|
+
sort_order: str = "asc",
|
|
173
|
+
) -> Dict[str, Any]:
|
|
174
|
+
"""Get metrics for a specific entity.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
entity: The entity's slug
|
|
178
|
+
limit: Maximum number of records to return (default: 10000)
|
|
179
|
+
offset: Number of records to skip (default: 0)
|
|
180
|
+
order_by: Column to order by (default: "last_updated")
|
|
181
|
+
sort_order: Sort order ("asc" or "desc", default: "asc")
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
MetricResponse containing list of metrics for the entity
|
|
185
|
+
"""
|
|
186
|
+
response = self._make_request(
|
|
187
|
+
f"entity/{entity}/metrics",
|
|
188
|
+
{"limit": limit, "offset": offset, "sort_order": sort_order},
|
|
189
|
+
)
|
|
190
|
+
return response["records"]
|
|
191
|
+
|
|
192
|
+
def query(
|
|
193
|
+
self,
|
|
194
|
+
by: str,
|
|
195
|
+
id: str,
|
|
196
|
+
start_date: str,
|
|
197
|
+
end_date: str,
|
|
198
|
+
limit: int = 100,
|
|
199
|
+
offset: int = 0,
|
|
200
|
+
order_by: str = "release_date",
|
|
201
|
+
sort_order: str = "asc",
|
|
202
|
+
entity_filter: Optional[list[str]] = None,
|
|
203
|
+
) -> Dict[str, Any]:
|
|
204
|
+
"""Query by metric.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
by: The type of query to perform (either "metric" or "entity")
|
|
208
|
+
id: The metric or entity's id/slug
|
|
209
|
+
start_date: Start date (YYYY-MM-DD)
|
|
210
|
+
end_date: End date (YYYY-MM-DD)
|
|
211
|
+
limit: Maximum number of records to return (default: 100)
|
|
212
|
+
offset: Number of records to skip (default: 0)
|
|
213
|
+
order_by: Column to order by (either "release_date" or "series_label", default: "release_date")
|
|
214
|
+
sort_order: Sort order ("asc" or "desc", default: "desc")
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
QueryResponse containing list of query results
|
|
218
|
+
"""
|
|
219
|
+
if by not in ["metric", "entity"]:
|
|
220
|
+
raise InvalidInputError(f"Invalid input: {by}")
|
|
221
|
+
if order_by not in ["release_date", "series_label"]:
|
|
222
|
+
raise InvalidInputError(f"Invalid input: {order_by}")
|
|
223
|
+
if sort_order not in ["asc", "desc"]:
|
|
224
|
+
raise InvalidInputError(f"Invalid input: {sort_order}")
|
|
225
|
+
params = {
|
|
226
|
+
"start_date": start_date,
|
|
227
|
+
"end_date": end_date,
|
|
228
|
+
"limit": limit,
|
|
229
|
+
"offset": offset,
|
|
230
|
+
"sort_order": sort_order,
|
|
231
|
+
"order_by": order_by,
|
|
232
|
+
}
|
|
233
|
+
if entity_filter is not None:
|
|
234
|
+
params["entities"] = entity_filter
|
|
235
|
+
response = self._make_request(f"query/{by}/{id}", params)
|
|
236
|
+
return response["records"]
|
|
237
|
+
|
|
238
|
+
def search_for_entity(
|
|
239
|
+
self,
|
|
240
|
+
query: str,
|
|
241
|
+
limit: int = 3,
|
|
242
|
+
offset: int = 0,
|
|
243
|
+
) -> Dict[str, Any]:
|
|
244
|
+
"""Search for an entity.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
query: The query to search for
|
|
248
|
+
offset: Number of records to skip (default: 0)
|
|
249
|
+
limit: Maximum number of records to return (default: 10000)
|
|
250
|
+
sort_order: Sort order ("asc" or "desc", default: "asc")
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
SearchResponse containing list of search results
|
|
254
|
+
"""
|
|
255
|
+
response = self._make_request(
|
|
256
|
+
f"search/entity", {"query": query, "offset": offset, "limit": limit}
|
|
257
|
+
)
|
|
258
|
+
return response["records"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from io import StringIO
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from tabulate import tabulate
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def common_params(f):
|
|
10
|
+
"""
|
|
11
|
+
Decorator to apply common CLI parameters: limit, offset, and format.
|
|
12
|
+
"""
|
|
13
|
+
f = click.option(
|
|
14
|
+
"--limit",
|
|
15
|
+
default=100,
|
|
16
|
+
help="Maximum number of records to return (default: 10000)",
|
|
17
|
+
)(f)
|
|
18
|
+
f = click.option(
|
|
19
|
+
"--offset", default=0, help="Number of records to skip (default: 0)"
|
|
20
|
+
)(f)
|
|
21
|
+
f = click.option(
|
|
22
|
+
"--format",
|
|
23
|
+
default="pretty",
|
|
24
|
+
help="Output format. Valid formats are: json, csv, pretty.",
|
|
25
|
+
)(f)
|
|
26
|
+
return f
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def date_handler(date_str):
|
|
30
|
+
if len(date_str) in (10, 11): # YYYY-MM-DD or unix timestamp
|
|
31
|
+
return date_str
|
|
32
|
+
elif len(date_str) == 4: # year
|
|
33
|
+
return f"{date_str}-01-01"
|
|
34
|
+
elif len(date_str) == 7: # year-month
|
|
35
|
+
return f"{date_str}-01"
|
|
36
|
+
else:
|
|
37
|
+
raise ValueError(f"Invalid date format: {date_str}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def df_to_csv_string(df):
|
|
41
|
+
csv_buffer = StringIO()
|
|
42
|
+
df.to_csv(csv_buffer, index=False)
|
|
43
|
+
return csv_buffer.getvalue()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def format_and_print(response_data, format):
|
|
47
|
+
if format == "json":
|
|
48
|
+
click.echo(json.dumps(response_data))
|
|
49
|
+
elif format == "csv":
|
|
50
|
+
click.echo(df_to_csv_string(pd.DataFrame(response_data)))
|
|
51
|
+
elif format == "pretty":
|
|
52
|
+
if len(response_data) == 1:
|
|
53
|
+
response_data = [(k, v) for k, v in response_data[0].items()]
|
|
54
|
+
click.echo(
|
|
55
|
+
tabulate(response_data, headers=["key", "value"], tablefmt="pretty")
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
click.echo(tabulate(response_data, headers="keys", tablefmt="pretty"))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "jstdata"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A Python interface to the Jefferson Street REST API."
|
|
5
|
+
authors = ["Mahfuj Ahbab"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
homepage = "https://github.com/mahfuj/python_api"
|
|
9
|
+
repository = "https://github.com/mahfuj/python_api"
|
|
10
|
+
keywords = ["api", "cli", "data", "jefferson street"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.poetry.dependencies]
|
|
22
|
+
python = "^3.10"
|
|
23
|
+
click = "^8.2.1"
|
|
24
|
+
pandas = "^2.3.1"
|
|
25
|
+
tabulate = "^0.9.0"
|
|
26
|
+
|
|
27
|
+
[tool.poetry.group.dev.dependencies]
|
|
28
|
+
pytest = "^8.3.5"
|
|
29
|
+
requests-mock = "^1.12.1"
|
|
30
|
+
pytest-cov = "^6.1.1"
|
|
31
|
+
black = "^25.1.0"
|
|
32
|
+
isort = "^6.0.1"
|
|
33
|
+
flake8 = "^7.2.0"
|
|
34
|
+
mypy = "^1.15.0"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["poetry-core"]
|
|
38
|
+
build-backend = "poetry.core.masonry.api"
|
|
39
|
+
|
|
40
|
+
[tool.poetry.scripts]
|
|
41
|
+
jst = "jstdata.cli:cli"
|