From 8ff3c040a2c8c205b31566f1135e38a3c84293b2 Mon Sep 17 00:00:00 2001 From: Mochan <103326951+0xMochan@users.noreply.github.com> Date: Sun, 2 Apr 2023 13:33:09 -0700 Subject: [PATCH 1/2] feat: Headers Support (#13) * feat: add headers to Subgrounds * feat: add user-agent to headers Add env var support for api keys in future PR. --- subgrounds/client.py | 15 +++++++--- subgrounds/pagination/pagination.py | 28 +++++++++++++++---- subgrounds/subgrounds.py | 26 +++++++++++++---- subgrounds/utils.py | 43 +++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 16 deletions(-) diff --git a/subgrounds/client.py b/subgrounds/client.py index 96f5660..bd211b9 100644 --- a/subgrounds/client.py +++ b/subgrounds/client.py @@ -7,6 +7,8 @@ import requests +from subgrounds.utils import default_header + logger = logging.getLogger("subgrounds") @@ -101,7 +103,7 @@ """ -def get_schema(url: str) -> dict[str, Any]: +def get_schema(url: str, headers: dict[str, Any]) -> dict[str, Any]: """Runs the introspection query on the GraphQL API served localed at :attr:`url` and returns the result. In case of errors, an exception containing the error message is thrown. @@ -118,7 +120,7 @@ def get_schema(url: str) -> dict[str, Any]: resp = requests.post( url, json={"query": INTROSPECTION_QUERY}, - headers={"Content-Type": "application/json"}, + headers=default_header() | headers, ).json() try: @@ -127,7 +129,12 @@ def get_schema(url: str) -> dict[str, Any]: raise Exception(resp["errors"]) from exn -def query(url: str, query_str: str, variables: dict[str, Any] = {}) -> dict[str, Any]: +def query( + url: str, + query_str: str, + variables: dict[str, Any] = {}, + headers: dict[str, Any] = {}, +) -> dict[str, Any]: """Executes the GraphQL query :attr:`query_str` with variables :attr:`variables` against the API served at :attr:`url` and returns the response data. In case of errors, an exception containing the error message is @@ -154,7 +161,7 @@ def query(url: str, query_str: str, variables: dict[str, Any] = {}) -> dict[str, if variables == {} else {"query": query_str, "variables": variables} ), - headers={"Content-Type": "application/json"}, + headers=default_header() | headers, ).json() try: diff --git a/subgrounds/pagination/pagination.py b/subgrounds/pagination/pagination.py index 73797f9..8816884 100644 --- a/subgrounds/pagination/pagination.py +++ b/subgrounds/pagination/pagination.py @@ -54,7 +54,10 @@ def step( def paginate( - schema: SchemaMeta, doc: Document, pagination_strategy: Type[PaginationStrategy] + schema: SchemaMeta, + doc: Document, + pagination_strategy: Type[PaginationStrategy], + headers: dict[str, Any], ) -> dict[str, Any]: """Executes the request document `doc` based on the GraphQL schema `schema` and returns the response as a JSON dictionary. @@ -76,7 +79,10 @@ def paginate( while True: try: page_data = client.query( - url=doc.url, query_str=doc.graphql, variables=doc.variables | args + url=doc.url, + query_str=doc.graphql, + variables=doc.variables | args, + headers=headers, ) data = merge(data, page_data) doc, args = strategy.step(page_data) @@ -88,11 +94,16 @@ def paginate( return data except SkipPagination: - return client.query(doc.url, doc.graphql, variables=doc.variables) + return client.query( + doc.url, doc.graphql, variables=doc.variables, headers=headers + ) def paginate_iter( - schema: SchemaMeta, doc: Document, pagination_strategy: Type[PaginationStrategy] + schema: SchemaMeta, + doc: Document, + pagination_strategy: Type[PaginationStrategy], + headers: dict[str, Any], ) -> Iterator[dict[str, Any]]: """Executes the request document `doc` based on the GraphQL schema `schema` and returns the response as a JSON dictionary. @@ -113,7 +124,10 @@ def paginate_iter( while True: try: page_data = client.query( - url=doc.url, query_str=doc.graphql, variables=doc.variables | args + url=doc.url, + query_str=doc.graphql, + variables=doc.variables | args, + headers=headers, ) yield page_data doc, args = strategy.step(page_data) @@ -123,4 +137,6 @@ def paginate_iter( raise PaginationError(exn.args[0], strategy) except SkipPagination: - return client.query(doc.url, doc.graphql, variables=doc.variables) + return client.query( + doc.url, doc.graphql, variables=doc.variables, headers=headers + ) diff --git a/subgrounds/subgrounds.py b/subgrounds/subgrounds.py index 5b49fc6..2f35d7e 100644 --- a/subgrounds/subgrounds.py +++ b/subgrounds/subgrounds.py @@ -54,6 +54,7 @@ def subgraph_slug(url: str) -> str: @dataclass class Subgrounds: + headers: dict[str, Any] = field(default_factory=dict) global_transforms: list[RequestTransform] = field( default_factory=lambda: DEFAULT_GLOBAL_TRANSFORMS ) @@ -76,11 +77,11 @@ def load( if schema_path.exists(): schema = load_schema(schema_path) else: - schema = client.get_schema(url) + schema = client.get_schema(url, headers=self.headers) store_schema(schema, schema_path) else: - schema = client.get_schema(url) + schema = client.get_schema(url, headers=self.headers) subgraph = Subgraph( url, @@ -183,10 +184,15 @@ def execute_document(doc: Document) -> dict: ) if pagination_strategy is not None and subgraph._is_subgraph: return paginate( - subgraph._schema, doc, pagination_strategy=pagination_strategy + subgraph._schema, + doc, + pagination_strategy=pagination_strategy, + headers=self.headers, ) else: - return client.query(doc.url, doc.graphql, variables=doc.variables) + return client.query( + doc.url, doc.graphql, variables=doc.variables, headers=headers + ) def transform_doc(transforms: list[DocumentTransform], doc: Document) -> dict: logger.debug(f"execute.transform_doc: doc = \n{doc.graphql}") @@ -246,10 +252,18 @@ def execute_document(doc: Document) -> Iterator[dict[str, Any]]: ) if pagination_strategy is not None and subgraph._is_subgraph: yield from paginate_iter( - subgraph._schema, doc, pagination_strategy=pagination_strategy + subgraph._schema, + doc, + pagination_strategy=pagination_strategy, + headers=self.headers, ) else: - yield client.query(doc.url, doc.graphql, variables=doc.variables) + yield client.query( + doc.url, + doc.graphql, + variables=doc.variables, + headers=self.headers, + ) def transform_doc( transforms: list[DocumentTransform], doc: Document diff --git a/subgrounds/utils.py b/subgrounds/utils.py index a439f4e..2a41875 100644 --- a/subgrounds/utils.py +++ b/subgrounds/utils.py @@ -2,6 +2,8 @@ Utility module for Subgrounds """ +import platform +from functools import cache from itertools import filterfalse from typing import Any, Callable, Iterator, Optional, Tuple, TypeVar @@ -189,3 +191,44 @@ def contains_list(data: dict | list | str | int | float | bool) -> bool: # columns.append('_'.join([*keys, key])) # return columns + +# ================================================================ +# User Agent / Headers +# ================================================================ + + +@cache +def default_header(): + """Contains the default header information for requests made by subgrounds""" + + return {"Content-Type": "application/json", "User-Agent": user_agent()} + + +@cache +def user_agent(): + """A basic user agent describing the following details: + + - "Subgrounds" (and version) + - Platform/OS (and architecture) + - Python Type (and version) + + Examples: + - Subgrounds/1.1.2 (Darwin; arm64) CPython/3.11.2 + - Subgrounds/1.1.2 (Emscripten; wasm32) CPython/3.10.2 + + ⚠️ To override this, construct your :class:`~subgrounds.Subgrounds` with a headers + parameter with a dictionary containing an empty "User-Agent" key-value pairing. + """ + + from subgrounds import __version__ # avoid import loops + + system = platform.system() + python = platform.python_implementation() + python_version = platform.python_version() + machine = platform.machine() + + return ( + f"Subgrounds/{__version__}" + f" ({system}; {machine})" + f" {python}/{python_version}" + ) From 36f36cd9e7e2377a604e2a4d9eea7bb5f1bf0edd Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 2 Apr 2023 20:34:54 +0000 Subject: [PATCH 2/2] 1.2.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- subgrounds/__init__.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a60dc35..a9a4cff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## v1.2.0 (2023-04-02) +### Feature +* Headers Support ([#13](https://github.com/0xPlaygrounds/subgrounds/issues/13)) ([`8ff3c04`](https://github.com/0xPlaygrounds/subgrounds/commit/8ff3c040a2c8c205b31566f1135e38a3c84293b2)) + +### Documentation +* Fix changelog formatting ([`7f8a630`](https://github.com/0xPlaygrounds/subgrounds/commit/7f8a6302871387042ffa0ea323284832f2611ea7)) + ## v1.1.2 (2023-03-15) ### Fix * `Query.map` had no default value for `priority` ([#12](https://github.com/0xPlaygrounds/subgrounds/issues/12)) ([`82a5f29`](https://github.com/0xPlaygrounds/subgrounds/commit/82a5f293e4cf7398350d12e309cd3cff190c1318)) diff --git a/pyproject.toml b/pyproject.toml index fadf5fa..d0691a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "subgrounds" -version = "1.1.2" +version = "1.2.0" description = "A Pythonic data access layer for applications querying data from The Graph Network." authors = [ "cvauclair ", diff --git a/subgrounds/__init__.py b/subgrounds/__init__.py index 4e3a145..55c2320 100644 --- a/subgrounds/__init__.py +++ b/subgrounds/__init__.py @@ -1,7 +1,7 @@ from subgrounds.subgraph import FieldPath, Subgraph, SyntheticField from subgrounds.subgrounds import Subgrounds -__version__ = "1.1.2" +__version__ = "1.2.0" __all__ = [ "FieldPath",