garf-core 0.0.12__tar.gz → 0.1.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: garf-core
3
- Version: 0.0.12
3
+ Version: 0.1.1
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
 
@@ -8,15 +8,13 @@
8
8
 
9
9
  These abstractions are designed to be as modular and simple as possible:
10
10
 
11
- * `BaseApiClient` - an interface for connecting to APIs.
12
- * `BaseQuery` - encapsulates SQL-like request.
11
+ * `BaseApiClient` - an interface for connecting to APIs. Check [default implementations](docs/builtin-functionality.md#apiclients)
12
+ * `BaseParser` - an interface to parse results from the API. Check [default implementations](docs/builtin-functionality.md#parsers)
13
+ * `ApiReportFetcher` - responsible for fetching and parsing data from reporting API. [Default implementations](docs/builtin-functionality.md#apireportfetchers)
14
+
13
15
  * `QuerySpecification` - parsed SQL-query into various elements.
14
- * `BaseParser` - an interface to parse results from the API. Have a couple of default implementations:
15
- * `ListParser` - returns results from API as a raw list.
16
- * `DictParser` - returns results from API as a formatted dict.
17
- * `NumericDictParser` - returns results from API as a formatted dict with converted numeric values.
16
+ * `BaseQuery` - protocol for all class based queries.
18
17
  * `GarfReport` - contains data from API in a format that is easy to write and interact with.
19
- * `ApiReportFetcher` - responsible for fetching and parsing data from reporting API.
20
18
 
21
19
  ## Installation
22
20
 
@@ -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.1'
@@ -0,0 +1,172 @@
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
+ """Module for defining client to interact with API."""
15
+
16
+ from __future__ import annotations
17
+
18
+ import abc
19
+ import contextlib
20
+ import csv
21
+ import dataclasses
22
+ import json
23
+ import os
24
+ import pathlib
25
+ from collections.abc import Sequence
26
+ from typing import Any
27
+
28
+ import requests
29
+ from typing_extensions import override
30
+
31
+ from garf_core import exceptions
32
+
33
+
34
+ @dataclasses.dataclass
35
+ class GarfApiRequest:
36
+ """Base class for specifying request."""
37
+
38
+
39
+ @dataclasses.dataclass
40
+ class GarfApiResponse:
41
+ """Base class for specifying response."""
42
+
43
+ results: list
44
+
45
+
46
+ class GarfApiError(exceptions.GarfError):
47
+ """API specific exception."""
48
+
49
+
50
+ class BaseClient(abc.ABC):
51
+ """Base API client class."""
52
+
53
+ @abc.abstractmethod
54
+ def get_response(
55
+ self, request: GarfApiRequest = GarfApiRequest(), **kwargs: str
56
+ ) -> GarfApiResponse:
57
+ """Method for getting response."""
58
+
59
+
60
+ class RestApiClient(BaseClient):
61
+ """Specifies REST client."""
62
+
63
+ OK = 200
64
+
65
+ def __init__(self, endpoint: str, **kwargs: str) -> None:
66
+ """Initializes RestApiClient."""
67
+ self.endpoint = endpoint
68
+ self.query_args = kwargs
69
+
70
+ @override
71
+ def get_response(
72
+ self, request: GarfApiRequest = GarfApiRequest(), **kwargs: str
73
+ ) -> GarfApiResponse:
74
+ response = requests.get(f'{self.endpoint}/{request.resource_name}')
75
+ if response.status_code == self.OK:
76
+ return GarfApiResponse(response.json())
77
+ raise GarfApiError('Failed to get data from API')
78
+
79
+
80
+ class FakeApiClient(BaseClient):
81
+ """Fake class for specifying API client."""
82
+
83
+ def __init__(self, results: Sequence[dict[str, Any]], **kwargs: str) -> None:
84
+ """Initializes FakeApiClient."""
85
+ self.results = list(results)
86
+ self.kwargs = kwargs
87
+
88
+ @override
89
+ def get_response(
90
+ self, request: GarfApiRequest = GarfApiRequest(), **kwargs: str
91
+ ) -> GarfApiResponse:
92
+ del request
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,80 @@
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
+ from collections.abc import Sequence
24
+ from typing import Any
25
+
26
+ from garf_core import (
27
+ api_clients,
28
+ parsers,
29
+ query_editor,
30
+ report_fetcher,
31
+ )
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class FakeApiReportFetcher(report_fetcher.ApiReportFetcher):
37
+ """Returns simulated data."""
38
+
39
+ def __init__(
40
+ self,
41
+ api_client: api_clients.FakeApiClient | None = None,
42
+ parser: parsers.BaseParser = parsers.DictParser,
43
+ query_specification_builder: query_editor.QuerySpecification = (
44
+ query_editor.QuerySpecification
45
+ ),
46
+ data_location: str | os.PathLike[str] | None = None,
47
+ csv_location: str | os.PathLike[str] | None = None,
48
+ json_location: str | os.PathLike[str] | None = None,
49
+ **kwargs: str,
50
+ ) -> None:
51
+ if not api_client and not (
52
+ data_location := json_location or csv_location or data_location
53
+ ):
54
+ raise report_fetcher.ApiReportFetcherError(
55
+ 'Missing fake data for the fetcher.'
56
+ )
57
+ if not api_client:
58
+ api_client = api_clients.FakeApiClient.from_file(data_location)
59
+ super().__init__(api_client, parser, query_specification_builder, **kwargs)
60
+
61
+ @classmethod
62
+ def from_data(cls, data: Sequence[dict[str, Any]]) -> FakeApiReportFetcher:
63
+ """Initializes FakeApiReportFetcher from a sequence of data."""
64
+ return FakeApiReportFetcher(
65
+ api_client=api_clients.FakeApiClient(results=data)
66
+ )
67
+
68
+ @classmethod
69
+ def from_csv(
70
+ cls, file_location: str | os.PathLike[str]
71
+ ) -> FakeApiReportFetcher:
72
+ """Initializes FakeApiReportFetcher from a csv file."""
73
+ return FakeApiReportFetcher(csv_location=file_location)
74
+
75
+ @classmethod
76
+ def from_json(
77
+ cls, file_location: str | os.PathLike[str]
78
+ ) -> FakeApiReportFetcher:
79
+ """Initializes FakeApiReportFetcher from a json file."""
80
+ return FakeApiReportFetcher(json_location=file_location)
@@ -0,0 +1,71 @@
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
+ api_client: api_clients.RestApiClient | None = None,
44
+ parser: parsers.BaseParser = parsers.DictParser,
45
+ query_specification_builder: query_editor.QuerySpecification = (
46
+ query_editor.QuerySpecification
47
+ ),
48
+ endpoint: str | None = None,
49
+ **kwargs: str,
50
+ ) -> None:
51
+ """Instantiates RestApiReportFetcher.
52
+
53
+ Args:
54
+ endpoint: URL of API endpoint.
55
+ parser: Type of parser to convert API response.
56
+ query_specification_builder: Class to perform query parsing.
57
+ """
58
+ if not api_client and not endpoint:
59
+ raise report_fetcher.ApiReportFetcherError(
60
+ 'Missing api_client or endpoint for the fetcher.'
61
+ )
62
+ if not api_client:
63
+ api_client = api_clients.RestApiClient(endpoint)
64
+ super().__init__(api_client, parser, query_specification_builder, **kwargs)
65
+
66
+ @classmethod
67
+ def from_endpoint(cls, endpoint: str) -> RestApiReportFetcher:
68
+ """Initializes RestApiReportFetcher: from an API endpoint."""
69
+ return RestApiReportFetcher(
70
+ api_client=api_clients.RestApiClient(endpoint=endpoint)
71
+ )
@@ -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.1
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
 
@@ -13,4 +13,7 @@ garf_core.egg-info/SOURCES.txt
13
13
  garf_core.egg-info/dependency_links.txt
14
14
  garf_core.egg-info/entry_points.txt
15
15
  garf_core.egg-info/requires.txt
16
- garf_core.egg-info/top_level.txt
16
+ garf_core.egg-info/top_level.txt
17
+ garf_core/fetchers/__init__.py
18
+ garf_core/fetchers/fake.py
19
+ garf_core/fetchers/rest.py
@@ -0,0 +1,3 @@
1
+ [garf]
2
+ fake = garf_core.fetchers.fake
3
+ rest = garf_core.fetchers.rest
@@ -39,7 +39,8 @@ dynamic=["version"]
39
39
  version = {attr = "garf_core.__version__"}
40
40
 
41
41
  [project.entry-points.garf]
42
- rest = "garf_core.report_fetcher"
42
+ rest = "garf_core.fetchers.rest"
43
+ fake = "garf_core.fetchers.fake"
43
44
 
44
45
  [project.optional-dependencies]
45
46
  pandas=[
@@ -1,86 +0,0 @@
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
- """Module for defining client to interact with API."""
15
-
16
- from __future__ import annotations
17
-
18
- import abc
19
- import dataclasses
20
- from collections.abc import Sequence
21
-
22
- import requests
23
- from typing_extensions import override
24
-
25
- from garf_core import exceptions
26
-
27
-
28
- @dataclasses.dataclass
29
- class GarfApiRequest:
30
- """Base class for specifying request."""
31
-
32
-
33
- @dataclasses.dataclass
34
- class GarfApiResponse:
35
- """Base class for specifying response."""
36
-
37
- results: list
38
-
39
-
40
- class GarfApiError(exceptions.GarfError):
41
- """API specific exception."""
42
-
43
-
44
- class BaseClient(abc.ABC):
45
- """Base API client class."""
46
-
47
- @abc.abstractmethod
48
- def get_response(
49
- self, request: GarfApiRequest = GarfApiRequest(), **kwargs: str
50
- ) -> GarfApiResponse:
51
- """Method for getting response."""
52
-
53
-
54
- class RestApiClient(BaseClient):
55
- """Specifies REST client."""
56
-
57
- OK = 200
58
-
59
- def __init__(self, endpoint: str, **kwargs: str) -> None:
60
- """Initializes RestApiClient."""
61
- self.endpoint = endpoint
62
- self.query_args = kwargs
63
-
64
- @override
65
- def get_response(
66
- self, request: GarfApiRequest = GarfApiRequest(), **kwargs: str
67
- ) -> GarfApiResponse:
68
- response = requests.get(f'{self.endpoint}/{request.resource_name}')
69
- if response.status_code == self.OK:
70
- return GarfApiResponse(response.json())
71
- raise GarfApiError('Failed to get data from API')
72
-
73
-
74
- class FakeApiClient(BaseClient):
75
- """Fake class for specifying API client."""
76
-
77
- def __init__(self, results: Sequence) -> None:
78
- """Initializes FakeApiClient."""
79
- self.results = list(results)
80
-
81
- @override
82
- def get_response(
83
- self, request: GarfApiRequest = GarfApiRequest()
84
- ) -> GarfApiResponse:
85
- del request
86
- return GarfApiResponse(results=self.results)
@@ -1,2 +0,0 @@
1
- [garf]
2
- rest = garf_core.report_fetcher
File without changes