garf-core 0.0.12__py3-none-any.whl → 0.1.1__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 +1 -1
- garf_core/api_clients.py +88 -2
- garf_core/fetchers/__init__.py +23 -0
- garf_core/fetchers/fake.py +80 -0
- garf_core/fetchers/rest.py +71 -0
- garf_core/report_fetcher.py +8 -32
- {garf_core-0.0.12.dist-info → garf_core-0.1.1.dist-info}/METADATA +6 -8
- garf_core-0.1.1.dist-info/RECORD +16 -0
- garf_core-0.1.1.dist-info/entry_points.txt +3 -0
- garf_core-0.0.12.dist-info/RECORD +0 -13
- garf_core-0.0.12.dist-info/entry_points.txt +0 -2
- {garf_core-0.0.12.dist-info → garf_core-0.1.1.dist-info}/WHEEL +0 -0
- {garf_core-0.0.12.dist-info → garf_core-0.1.1.dist-info}/top_level.txt +0 -0
garf_core/__init__.py
CHANGED
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,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
|
+
)
|
garf_core/report_fetcher.py
CHANGED
@@ -16,17 +16,17 @@
|
|
16
16
|
|
17
17
|
"""Module for getting data from API based on a query.
|
18
18
|
|
19
|
-
ApiReportFetcher
|
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
|
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.
|
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.
|
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
|
-
* `
|
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
|
-
* `
|
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=5D5GNZp_18xDdFHuq7q1jn0HbR0-HD0s4vn0qzAAGKo,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=fgJjxuHyd6EIUflUtj8r_HfaMS1YTDrOqDlaj6Kvbjs,2584
|
11
|
+
garf_core/fetchers/rest.py,sha256=-5B2-Ck_t3hG99ym59AKwlzctiDxRFI2Nnc8STBxRDo,2201
|
12
|
+
garf_core-0.1.1.dist-info/METADATA,sha256=OY4D0XX-hTCKypRrrea9bBYuqpR87Z58Y5WK35X31zo,2443
|
13
|
+
garf_core-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
14
|
+
garf_core-0.1.1.dist-info/entry_points.txt,sha256=u4h-ujHO1hbxVXRQzwcC4ftju9_KBYtq5mCLKEBHMj0,69
|
15
|
+
garf_core-0.1.1.dist-info/top_level.txt,sha256=Gj-Zp7fM2turGut5vTJuo5xEcKfow7cTLX3y3WFfNgA,10
|
16
|
+
garf_core-0.1.1.dist-info/RECORD,,
|
@@ -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,,
|
File without changes
|
File without changes
|