garf-core 0.0.12__py3-none-any.whl → 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.
garf_core/__init__.py CHANGED
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = '0.0.12'
15
+ __version__ = '0.1.0'
garf_core/api_clients.py CHANGED
@@ -16,8 +16,14 @@
16
16
  from __future__ import annotations
17
17
 
18
18
  import abc
19
+ import contextlib
20
+ import csv
19
21
  import dataclasses
22
+ import json
23
+ import os
24
+ import pathlib
20
25
  from collections.abc import Sequence
26
+ from typing import Any
21
27
 
22
28
  import requests
23
29
  from typing_extensions import override
@@ -74,13 +80,93 @@ class RestApiClient(BaseClient):
74
80
  class FakeApiClient(BaseClient):
75
81
  """Fake class for specifying API client."""
76
82
 
77
- def __init__(self, results: Sequence) -> None:
83
+ def __init__(self, results: Sequence[dict[str, Any]], **kwargs: str) -> None:
78
84
  """Initializes FakeApiClient."""
79
85
  self.results = list(results)
86
+ self.kwargs = kwargs
80
87
 
81
88
  @override
82
89
  def get_response(
83
- self, request: GarfApiRequest = GarfApiRequest()
90
+ self, request: GarfApiRequest = GarfApiRequest(), **kwargs: str
84
91
  ) -> GarfApiResponse:
85
92
  del request
86
93
  return GarfApiResponse(results=self.results)
94
+
95
+ @classmethod
96
+ def from_file(cls, file_location: str | os.PathLike[str]) -> FakeApiClient:
97
+ """Initializes FakeApiClient from json or csv files.
98
+
99
+ Args:
100
+ file_location: Path of file with data.
101
+
102
+ Returns:
103
+ Initialized client.
104
+
105
+ Raises:
106
+ GarfApiError: When file with unsupported extension is provided.
107
+ """
108
+ if str(file_location).endswith('.json'):
109
+ return FakeApiClient.from_json(file_location)
110
+ if str(file_location).endswith('.csv'):
111
+ return FakeApiClient.from_csv(file_location)
112
+ raise GarfApiError(
113
+ 'Unsupported file extension, only csv and json are supported.'
114
+ )
115
+
116
+ @classmethod
117
+ def from_json(cls, file_location: str | os.PathLike[str]) -> FakeApiClient:
118
+ """Initializes FakeApiClient from json file.
119
+
120
+ Args:
121
+ file_location: Path of file with data.
122
+
123
+ Returns:
124
+ Initialized client.
125
+
126
+ Raises:
127
+ GarfApiError: When file with data not found.
128
+ """
129
+ try:
130
+ with pathlib.Path.open(file_location, 'r', encoding='utf-8') as f:
131
+ data = json.load(f)
132
+ return FakeApiClient(data)
133
+ except FileNotFoundError as e:
134
+ raise GarfApiError(f'Failed to open {file_location}') from e
135
+
136
+ @classmethod
137
+ def from_csv(cls, file_location: str | os.PathLike[str]) -> FakeApiClient:
138
+ """Initializes FakeApiClient from csv file.
139
+
140
+ Args:
141
+ file_location: Path of file with data.
142
+
143
+ Returns:
144
+ Initialized client.
145
+
146
+ Raises:
147
+ GarfApiError: When file with data not found.
148
+ """
149
+ try:
150
+ with pathlib.Path.open(file_location, 'r', encoding='utf-8') as f:
151
+ reader = csv.DictReader(f)
152
+ data = []
153
+ for row in reader:
154
+ data.append(
155
+ {key: _field_converter(value) for key, value in row.items()}
156
+ )
157
+ return FakeApiClient(data)
158
+ except FileNotFoundError as e:
159
+ raise GarfApiError(f'Failed to open {file_location}') from e
160
+
161
+
162
+ def _field_converter(field: str):
163
+ if isinstance(field, str) and (lower_field := field.lower()) in (
164
+ 'true',
165
+ 'false',
166
+ ):
167
+ return lower_field == 'true'
168
+ with contextlib.suppress(ValueError):
169
+ return int(field)
170
+ with contextlib.suppress(ValueError):
171
+ return float(field)
172
+ return field
@@ -0,0 +1,23 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Built-in fetchers."""
16
+
17
+ from garf_core.fetchers.fake import FakeApiReportFetcher
18
+ from garf_core.fetchers.rest import RestApiReportFetcher
19
+
20
+ __all__ = [
21
+ 'FakeApiReportFetcher',
22
+ 'RestApiReportFetcher',
23
+ ]
@@ -0,0 +1,74 @@
1
+ # Copyright 2025 Google LLf
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # pylint: disable=C0330, g-bad-import-order, g-multiple-import
16
+
17
+ """Getting fake data from memory or a file."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import os
23
+
24
+ from garf_core import (
25
+ api_clients,
26
+ parsers,
27
+ query_editor,
28
+ report_fetcher,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class FakeApiReportFetcher(report_fetcher.ApiReportFetcher):
35
+ """Returns simulated data."""
36
+
37
+ def __init__(
38
+ self,
39
+ data: list[dict[str, parsers.ApiRowElement]] | None = None,
40
+ parser: parsers.BaseParser = parsers.DictParser,
41
+ query_specification_builder: query_editor.QuerySpecification = (
42
+ query_editor.QuerySpecification
43
+ ),
44
+ data_location: str | os.PathLike[str] | None = None,
45
+ csv_location: str | os.PathLike[str] | None = None,
46
+ json_location: str | os.PathLike[str] | None = None,
47
+ **kwargs: str,
48
+ ) -> None:
49
+ if not data and not (
50
+ data_location := json_location or csv_location or data_location
51
+ ):
52
+ raise report_fetcher.ApiReportFetcherError(
53
+ 'Missing fake data for the fetcher.'
54
+ )
55
+ api_client = (
56
+ api_clients.FakeApiClient(data)
57
+ if data
58
+ else api_clients.FakeApiClient.from_file(data_location)
59
+ )
60
+ super().__init__(api_client, parser, query_specification_builder, **kwargs)
61
+
62
+ @classmethod
63
+ def from_csv(
64
+ cls, file_location: str | os.PathLike[str]
65
+ ) -> FakeApiReportFetcher:
66
+ """Initialized FakeApiReportFetcher from a csv file."""
67
+ return FakeApiReportFetcher(csv_location=file_location)
68
+
69
+ @classmethod
70
+ def from_json(
71
+ cls, file_location: str | os.PathLike[str]
72
+ ) -> FakeApiReportFetcher:
73
+ """Initialized FakeApiReportFetcher from a json file."""
74
+ return FakeApiReportFetcher(json_location=file_location)
@@ -0,0 +1,58 @@
1
+ # Copyright 2025 Google LLf
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # pylint: disable=C0330, g-bad-import-order, g-multiple-import
16
+
17
+ """Module for getting data from Rest APIs based on a query."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+
23
+ from garf_core import (
24
+ api_clients,
25
+ parsers,
26
+ query_editor,
27
+ report_fetcher,
28
+ )
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class RestApiReportFetcher(report_fetcher.ApiReportFetcher):
34
+ """Fetches data from an REST API endpoint.
35
+
36
+ Attributes:
37
+ api_client: Initialized RestApiClient.
38
+ parser: Type of parser to convert API response.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ endpoint: str,
44
+ parser: parsers.BaseParser = parsers.DictParser,
45
+ query_specification_builder: query_editor.QuerySpecification = (
46
+ query_editor.QuerySpecification
47
+ ),
48
+ **kwargs: str,
49
+ ) -> None:
50
+ """Instantiates RestApiReportFetcher.
51
+
52
+ Args:
53
+ endpoint: URL of API endpoint.
54
+ parser: Type of parser to convert API response.
55
+ query_specification_builder: Class to perform query parsing.
56
+ """
57
+ api_client = api_clients.RestApiClient(endpoint)
58
+ super().__init__(api_client, parser, query_specification_builder, **kwargs)
@@ -16,17 +16,17 @@
16
16
 
17
17
  """Module for getting data from API based on a query.
18
18
 
19
- ApiReportFetcher performs fetching data from API, parsing it
20
- and returning GarfReport.
19
+ ApiReportFetcher fetches data from API, parses it and returns GarfReport.
21
20
  """
22
21
 
23
22
  from __future__ import annotations
24
23
 
25
24
  import logging
26
- from typing import Any, Callable
25
+ from typing import Callable
27
26
 
28
27
  from garf_core import (
29
28
  api_clients,
29
+ exceptions,
30
30
  parsers,
31
31
  query_editor,
32
32
  report,
@@ -35,6 +35,10 @@ from garf_core import (
35
35
  logger = logging.getLogger(__name__)
36
36
 
37
37
 
38
+ class ApiReportFetcherError(exceptions.GarfError):
39
+ """Base exception for all ApiReportFetchers."""
40
+
41
+
38
42
  class ApiReportFetcher:
39
43
  """Class responsible for getting data from report API.
40
44
 
@@ -47,7 +51,7 @@ class ApiReportFetcher:
47
51
  def __init__(
48
52
  self,
49
53
  api_client: api_clients.BaseApiClient,
50
- parser: parsers.BaseParser = parsers.ListParser,
54
+ parser: parsers.BaseParser = parsers.DictParser,
51
55
  query_specification_builder: query_editor.QuerySpecification = (
52
56
  query_editor.QuerySpecification
53
57
  ),
@@ -135,31 +139,3 @@ class ApiReportFetcher:
135
139
  return report.GarfReport(
136
140
  results=parsed_response, column_names=query.column_names
137
141
  )
138
-
139
-
140
- class RestApiReportFetcher(ApiReportFetcher):
141
- """Fetches data from an REST API endpoint.
142
-
143
- Attributes:
144
- api_client: Initialized RestApiClient.
145
- parser: Type of parser to convert API response.
146
- """
147
-
148
- def __init__(
149
- self,
150
- endpoint: str,
151
- parser: parsers.BaseParser = parsers.DictParser,
152
- query_specification_builder: query_editor.QuerySpecification = (
153
- query_editor.QuerySpecification
154
- ),
155
- **kwargs: str,
156
- ) -> None:
157
- """Instantiates RestApiReportFetcher.
158
-
159
- Args:
160
- endpoint: URL of API endpoint.
161
- parser: Type of parser to convert API response.
162
- query_specification_builder: Class to perform query parsing.
163
- """
164
- api_client = api_clients.RestApiClient(endpoint)
165
- super().__init__(api_client, parser, query_specification_builder, **kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: garf-core
3
- Version: 0.0.12
3
+ Version: 0.1.0
4
4
  Summary: Abstracts fetching data from API based on provided SQL-like query.
5
5
  Author-email: "Google Inc. (gTech gPS CSE team)" <no-reply@google.com>, Andrei Markin <andrey.markin.ppc@gmail.com>
6
6
  License: Apache 2.0
@@ -40,15 +40,13 @@ Requires-Dist: garf-core[pandas,polars]; extra == "all"
40
40
 
41
41
  These abstractions are designed to be as modular and simple as possible:
42
42
 
43
- * `BaseApiClient` - an interface for connecting to APIs.
44
- * `BaseQuery` - encapsulates SQL-like request.
43
+ * `BaseApiClient` - an interface for connecting to APIs. Check [default implementations](docs/builtin-functionality.md#apiclients)
44
+ * `BaseParser` - an interface to parse results from the API. Check [default implementations](docs/builtin-functionality.md#parsers)
45
+ * `ApiReportFetcher` - responsible for fetching and parsing data from reporting API. [Default implementations](docs/builtin-functionality.md#apireportfetchers)
46
+
45
47
  * `QuerySpecification` - parsed SQL-query into various elements.
46
- * `BaseParser` - an interface to parse results from the API. Have a couple of default implementations:
47
- * `ListParser` - returns results from API as a raw list.
48
- * `DictParser` - returns results from API as a formatted dict.
49
- * `NumericDictParser` - returns results from API as a formatted dict with converted numeric values.
48
+ * `BaseQuery` - protocol for all class based queries.
50
49
  * `GarfReport` - contains data from API in a format that is easy to write and interact with.
51
- * `ApiReportFetcher` - responsible for fetching and parsing data from reporting API.
52
50
 
53
51
  ## Installation
54
52
 
@@ -0,0 +1,16 @@
1
+ garf_core/__init__.py,sha256=TVigAs35ZN82B6dDphIRXsuJqB2gTIaFv23NVzjAtT8,598
2
+ garf_core/api_clients.py,sha256=k4vjFsA3JOx7Hp_pXIQNwzMXqq4u4dDcz-ddkJC0JhI,4651
3
+ garf_core/base_query.py,sha256=ZDAw2ojmismXRO0HXEvKDukpS7OAc7390LnM8kvCSCY,1201
4
+ garf_core/exceptions.py,sha256=Gzvkl2M-rA_XQRAMd3CC62KHeFQE_b6uby0fD0pouw4,1269
5
+ garf_core/parsers.py,sha256=Uj7aT7roYUofivrBKVKWfcSeG37oYGGLIJ-op2C3hZc,3238
6
+ garf_core/query_editor.py,sha256=lA1aMFc_1wo6e2CyVISarnZxtH2jnKkuZfJecQqslDg,17191
7
+ garf_core/report.py,sha256=2z4tUR5mg29CkwodGaFIXs8Vpo2DCyyzwJWXhBrT0R4,19910
8
+ garf_core/report_fetcher.py,sha256=o6YT_o0t9EPutiJk9R637lQabkHSaOrzoF2ciOmpQPA,4560
9
+ garf_core/fetchers/__init__.py,sha256=_cSjg1D5RhUKxaVeVbaDdb8AAoI9glKJXgN5H4qXFkw,783
10
+ garf_core/fetchers/fake.py,sha256=Ba8EnSIZyNkTza3kfo2OlWWQSKcOawp1aIoyoX-Uw74,2313
11
+ garf_core/fetchers/rest.py,sha256=0d6sRVcxBSwWpMv5TH2L9lGGxad5vbK24q9-Ql8DcCc,1701
12
+ garf_core-0.1.0.dist-info/METADATA,sha256=V9eJEM7smpVTRquelpfDiWhqQUxIKGQSocoHpcVXmrQ,2443
13
+ garf_core-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ garf_core-0.1.0.dist-info/entry_points.txt,sha256=u4h-ujHO1hbxVXRQzwcC4ftju9_KBYtq5mCLKEBHMj0,69
15
+ garf_core-0.1.0.dist-info/top_level.txt,sha256=Gj-Zp7fM2turGut5vTJuo5xEcKfow7cTLX3y3WFfNgA,10
16
+ garf_core-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [garf]
2
+ fake = garf_core.fetchers.fake
3
+ rest = garf_core.fetchers.rest
@@ -1,13 +0,0 @@
1
- garf_core/__init__.py,sha256=ctwikDDSLAUSKCYv_lF4MLeza_na29fMcepnV6sNJrY,599
2
- garf_core/api_clients.py,sha256=S7Ldmgf0H9Vew2BdfG3Yh6imkhdnr8ix_YHKJmGln-U,2280
3
- garf_core/base_query.py,sha256=ZDAw2ojmismXRO0HXEvKDukpS7OAc7390LnM8kvCSCY,1201
4
- garf_core/exceptions.py,sha256=Gzvkl2M-rA_XQRAMd3CC62KHeFQE_b6uby0fD0pouw4,1269
5
- garf_core/parsers.py,sha256=Uj7aT7roYUofivrBKVKWfcSeG37oYGGLIJ-op2C3hZc,3238
6
- garf_core/query_editor.py,sha256=lA1aMFc_1wo6e2CyVISarnZxtH2jnKkuZfJecQqslDg,17191
7
- garf_core/report.py,sha256=2z4tUR5mg29CkwodGaFIXs8Vpo2DCyyzwJWXhBrT0R4,19910
8
- garf_core/report_fetcher.py,sha256=JK0k-svFQYCbSBYppgHCwuov5mW6_6CgYQhWusHlgXI,5269
9
- garf_core-0.0.12.dist-info/METADATA,sha256=UZaXQ3kk5mtm9c7-V6dgRicEIOLjW3CcWoMtz5givu4,2494
10
- garf_core-0.0.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- garf_core-0.0.12.dist-info/entry_points.txt,sha256=ODxSZXwWEIXQAjRwBQsBJ6GYw8oPK6DYTo0oDVkKCpo,39
12
- garf_core-0.0.12.dist-info/top_level.txt,sha256=Gj-Zp7fM2turGut5vTJuo5xEcKfow7cTLX3y3WFfNgA,10
13
- garf_core-0.0.12.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [garf]
2
- rest = garf_core.report_fetcher