From ad9d683c1fed10859623353664217840bbf8d727 Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Sun, 17 Sep 2023 17:45:14 -0400 Subject: [PATCH 01/14] feat: custom subgrounds client for polars --- .gitignore | 1 + subgrounds/contrib/polars/polars_client.py | 149 ++++++++++++++++++ subgrounds/contrib/polars/polars_utils.py | 61 +++++++ .../contrib/polars/test_polars_client.py | 55 +++++++ 4 files changed, 266 insertions(+) create mode 100644 subgrounds/contrib/polars/polars_client.py create mode 100644 subgrounds/contrib/polars/polars_utils.py create mode 100644 subgrounds/contrib/polars/test_polars_client.py diff --git a/.gitignore b/.gitignore index 7529ac0..fcdeef5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ subgrounds.egg-info/ # apple .DS_Store +.venv*/ \ No newline at end of file diff --git a/subgrounds/contrib/polars/polars_client.py b/subgrounds/contrib/polars/polars_client.py new file mode 100644 index 0000000..18db182 --- /dev/null +++ b/subgrounds/contrib/polars/polars_client.py @@ -0,0 +1,149 @@ +import functools +import json +import logging +import warnings +from collections.abc import Iterator +from contextlib import suppress +from functools import cached_property +from typing import Any, Type, cast +from json import JSONDecodeError + +import httpx +from pipe import map, traverse + +from subgrounds.dataframe_utils import df_of_json +from subgrounds.errors import GraphQLError, ServerError +from subgrounds.pagination import LegacyStrategy, PaginationStrategy +from subgrounds.query import DataRequest, DataResponse, Document, DocumentResponse +from subgrounds.subgraph import FieldPath, Subgraph +from subgrounds.utils import default_header +from subgrounds.client import SubgroundsBase + +logger = logging.getLogger("subgrounds") +warnings.simplefilter("default") + +HTTP2_SUPPORT = True + + +class SubgroundsPolars(SubgroundsBase): + # https://github.com/0xPlaygrounds/subgrounds/blob/feat/async/subgrounds/client/sync.py#L48 + # ? what is cached property decorator? 8/28/23 + @cached_property + def _client(self): + """Cached client""" + + return httpx.Client(http2=HTTP2_SUPPORT, timeout=self.timeout) + + def load( + self, + url: str, + save_schema: bool = False, + cache_dir: str | None = None, + is_subgraph: bool = True, + ) -> Subgraph: + if cache_dir is not None: + warnings.warn("This will be depreciated", DeprecationWarning) + + try: + loader = self._load(url, save_schema, is_subgraph) + url, query = next(loader) # if this fails, schema is loaded from cache + data = self._fetch(url, {"query": query}) + loader.send(data) + + except StopIteration as e: + return e.value + + assert False + + def load_subgraph( + self, url: str, save_schema: bool = False, cache_dir: str | None = None + ) -> Subgraph: + """Performs introspection on the provided GraphQL API ``url`` to get the + schema, stores the schema if ``save_schema`` is ``True`` and returns a + generated class representing the subgraph with all its entities. + + Args: + url The url of the API. + save_schema: Flag indicating whether or not the schema should be cached to + disk. + + Returns: + Subgraph: A generated class representing the subgraph and its entities + """ + + return self.load(url, save_schema, cache_dir, True) + + def _fetch(self, url: str, blob: dict[str, Any]) -> dict[str, Any]: + resp = self._client.post( + url, json=blob, headers=default_header(url) | self.headers + ) + resp.raise_for_status() + + try: + raw_data = resp.json() + + except JSONDecodeError: + raise ServerError( + f"Server ({url}) did not respond with proper JSON" + f"\nDid you query a proper GraphQL endpoint?" + f"\n\n{resp.content}" + ) + + if (data := raw_data.get("data")) is None: + raise GraphQLError(raw_data.get("errors", "Unknown Error(s) Found")) + + return data + + def execute( + self, + req: DataRequest, + pagination_strategy: Type[PaginationStrategy] | None = LegacyStrategy, + ) -> DataResponse: + """Executes a :class:`DataRequest` and returns a :class:`DataResponse`. + + Args: + req: The :class:`DataRequest` object to be executed. + pagination_strategy: A Class implementing the :class:`PaginationStrategy` + ``Protocol``. If ``None``, then automatic pagination is disabled. + Defaults to :class:`LegacyStrategy`. + + Returns: + A :class:`DataResponse` object representing the response + """ + + try: + executor = self._execute(req, pagination_strategy) + + doc = next(executor) + while True: + data = self._fetch( + doc.url, {"query": doc.graphql, "variables": doc.variables} + ) + doc = executor.send(DocumentResponse(url=doc.url, data=data)) + + except StopIteration as e: + return e.value + + def query_json( + self, + fpaths: FieldPath | list[FieldPath], + pagination_strategy: Type[PaginationStrategy] | None = LegacyStrategy, + ) -> list[dict[str, Any]]: + """Equivalent to + ``Subgrounds.execute(Subgrounds.mk_request(fpaths), pagination_strategy)``. + + Args: + fpaths: One or more :class:`FieldPath` objects + that should be included in the request. + pagination_strategy: A Class implementing the :class:`PaginationStrategy` + ``Protocol``. If ``None``, then automatic pagination is disabled. + Defaults to :class:`LegacyStrategy`. + + Returns: + The reponse data + """ + + fpaths = list([fpaths] | traverse | map(FieldPath._auto_select) | traverse) + req = self.mk_request(fpaths) + data = self.execute(req, pagination_strategy) + return [doc.data for doc in data.responses] diff --git a/subgrounds/contrib/polars/polars_utils.py b/subgrounds/contrib/polars/polars_utils.py new file mode 100644 index 0000000..b674621 --- /dev/null +++ b/subgrounds/contrib/polars/polars_utils.py @@ -0,0 +1,61 @@ +import polars as pl +import pyarrow as pa + +from time import time +from functools import wraps + +from typing import TYPE_CHECKING, Any + + +############################ +# Polars Support Functions - Convert GraphQL Response to Polars DataFrame +############################ + + +def fmt_dict_cols(df: pl.DataFrame) -> pl.DataFrame: + """ + formats dictionary cols, which are 'structs' in a polars df, into separate columns and renames accordingly. + """ + for column in df.columns: + if isinstance(df[column][0], dict): + col_names = df[column][0].keys() + # rename struct columns + struct_df = df.select( + pl.col(column).struct.rename_fields( + [f"{column}_{c}" for c in col_names] + ) + ) + struct_df = struct_df.unnest(column) + # add struct_df columns to df and + df = df.with_columns(struct_df) + # drop the df column + df = df.drop(column) + + return df + + +def fmt_arr_cols(df: pl.DataFrame) -> pl.DataFrame: + """ + formats lists, which are arrays in a polars df, into separate columns and renames accordingly. + Since there isn't a direct way to convert array -> new columns, we convert the array to a struct and then + unnest the struct into new columns. + """ + # use this logic if column is a list (rows show up as pl.Series) + for column in df.columns: + if isinstance(df[column][0], pl.Series): + # convert struct to array + struct_df = df.select([pl.col(column).arr.to_struct()]) + # rename struct fields + struct_df = struct_df.select( + pl.col(column).struct.rename_fields( + [f"{column}_{i}" for i in range(len(struct_df.shape))] + ) + ) + # unnest struct fields into their own columns + struct_df = struct_df.unnest(column) + # add struct_df columns to df and + df = df.with_columns(struct_df) + # drop the df column + df = df.drop(column) + + return df diff --git a/subgrounds/contrib/polars/test_polars_client.py b/subgrounds/contrib/polars/test_polars_client.py new file mode 100644 index 0000000..4fc73aa --- /dev/null +++ b/subgrounds/contrib/polars/test_polars_client.py @@ -0,0 +1,55 @@ +from polars_client import SubgroundsPolars +from subgrounds.subgraph import FieldPath, Subgraph + +# from polars_utils.py import * + +import polars as pl + +sg = SubgroundsPolars() + +snx_endpoint = "https://api.thegraph.com/subgraphs/name/synthetix-perps/perps" + +snx = sg.load_subgraph( + url=snx_endpoint, +) + +trades_json: list[dict] = sg.query_json( + [ + # set the first parameter to a larger size to query more rows. + snx.Query.futuresTrades( + first=2500, + orderBy="timestamp", + orderDirection="desc", + # where=[{"timestamp_lte": "1694131200"}], # 1694131200 = 9/8/23 + ) + ] +) + +json_trades_key = list(trades_json[0].keys())[0] +trades_df = pl.from_dicts( + trades_json[0][list(trades_json[0].keys())[0]], infer_schema_length=None +) + +print(trades_df.shape) +print(trades_df.head(5)) + +# 9/17/23 TODO - figure out correct import structure for fmt_dict_cols and fmt_arr_cols +# fmted_df = fmt_dict_cols(trades_df) + +# # polars calculations to convert big ints. All synthetix big ints are represented with 10**18 decimals. +# fmted_df = fmted_df.with_columns( +# [ +# (pl.col("margin") / 10**18), +# (pl.col("size") / 10**18), +# (pl.col("price") / 10**18), +# (pl.col("positionSize") / 10**18), +# (pl.col("realizedPnl") / 10**18), +# (pl.col("netFunding") / 10**18), +# (pl.col("feesPaidToSynthetix") / 10**18), +# # convert timestamp to datetime +# pl.from_epoch("timestamp").alias("datetime"), +# ] +# ) + + +print("done") From cc69f88e907e49a1f8fc39d3e3e0063ae3cf82ce Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Sun, 17 Sep 2023 17:55:40 -0400 Subject: [PATCH 02/14] fix: polars_utils import path, update test --- .../contrib/polars/test_polars_client.py | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/subgrounds/contrib/polars/test_polars_client.py b/subgrounds/contrib/polars/test_polars_client.py index 4fc73aa..f421928 100644 --- a/subgrounds/contrib/polars/test_polars_client.py +++ b/subgrounds/contrib/polars/test_polars_client.py @@ -1,7 +1,7 @@ from polars_client import SubgroundsPolars from subgrounds.subgraph import FieldPath, Subgraph -# from polars_utils.py import * +from polars_utils import * import polars as pl @@ -13,7 +13,7 @@ url=snx_endpoint, ) -trades_json: list[dict] = sg.query_json( +trades_json = sg.query_json( [ # set the first parameter to a larger size to query more rows. snx.Query.futuresTrades( @@ -30,26 +30,24 @@ trades_json[0][list(trades_json[0].keys())[0]], infer_schema_length=None ) -print(trades_df.shape) -print(trades_df.head(5)) - -# 9/17/23 TODO - figure out correct import structure for fmt_dict_cols and fmt_arr_cols -# fmted_df = fmt_dict_cols(trades_df) +fmted_df = fmt_dict_cols(trades_df) # # polars calculations to convert big ints. All synthetix big ints are represented with 10**18 decimals. -# fmted_df = fmted_df.with_columns( -# [ -# (pl.col("margin") / 10**18), -# (pl.col("size") / 10**18), -# (pl.col("price") / 10**18), -# (pl.col("positionSize") / 10**18), -# (pl.col("realizedPnl") / 10**18), -# (pl.col("netFunding") / 10**18), -# (pl.col("feesPaidToSynthetix") / 10**18), -# # convert timestamp to datetime -# pl.from_epoch("timestamp").alias("datetime"), -# ] -# ) +fmted_df = fmted_df.with_columns( + [ + (pl.col("margin") / 10**18), + (pl.col("size") / 10**18), + (pl.col("price") / 10**18), + (pl.col("positionSize") / 10**18), + (pl.col("realizedPnl") / 10**18), + (pl.col("netFunding") / 10**18), + (pl.col("feesPaidToSynthetix") / 10**18), + # convert timestamp to datetime + pl.from_epoch("timestamp").alias("datetime"), + ] +) +print(fmted_df.shape) +print(fmted_df.head(5)) print("done") From daed16c39d83b8593ead8312b2d0906069b3cef8 Mon Sep 17 00:00:00 2001 From: 0xMochan Date: Fri, 22 Sep 2023 16:11:47 -0400 Subject: [PATCH 03/14] fix: adjust naming + TODOs --- poetry.lock | 80 ++++++++++++++++++- pyproject.toml | 3 + subgrounds/contrib/polars/__init__.py | 3 + .../polars/{polars_client.py => client.py} | 27 +++---- .../polars/{polars_utils.py => utils.py} | 0 .../contrib/polars/test_client.py | 0 6 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 subgrounds/contrib/polars/__init__.py rename subgrounds/contrib/polars/{polars_client.py => client.py} (90%) rename subgrounds/contrib/polars/{polars_utils.py => utils.py} (100%) rename subgrounds/contrib/polars/test_polars_client.py => tests/contrib/polars/test_client.py (100%) diff --git a/poetry.lock b/poetry.lock index 4938412..6eb1779 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1403,6 +1403,41 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "polars" +version = "0.19.3" +description = "Blazingly fast DataFrame library" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "polars-0.19.3-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:cd407a847fe581af35dc4144420b9e6d6a639ddce4b5d2c6396719ad74140c40"}, + {file = "polars-0.19.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:2f033e8e4686bf02f923c18b679769bb5871f49b7c1e47a7c7071f272280477a"}, + {file = "polars-0.19.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43340deb657b8398ed2d972577756fb2431d64155cb9647881a35310d05be016"}, + {file = "polars-0.19.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d62122dca315e9ee0fcc52e58284c6d9d8c3163a420fd5a97441d5877746d76b"}, + {file = "polars-0.19.3-cp38-abi3-win_amd64.whl", hash = "sha256:e0990d8df05be5ff0ba2facd8ad7f33968e875f71aaf82d4924c23982434bd65"}, + {file = "polars-0.19.3.tar.gz", hash = "sha256:bef79f1742e6d1e7def1a4e664cafad7c406ff598c2b2b5867eff682920b2e7c"}, +] + +[package.extras] +adbc = ["adbc_driver_sqlite"] +all = ["polars[adbc,cloudpickle,connectorx,deltalake,fsspec,gevent,matplotlib,numpy,pandas,pyarrow,pydantic,sqlalchemy,timezone,xlsx2csv,xlsxwriter]"] +cloudpickle = ["cloudpickle"] +connectorx = ["connectorx"] +deltalake = ["deltalake (>=0.10.0)"] +fsspec = ["fsspec"] +gevent = ["gevent"] +matplotlib = ["matplotlib"] +numpy = ["numpy (>=1.16.0)"] +openpyxl = ["openpyxl (>=3.0.0)"] +pandas = ["pandas", "pyarrow (>=7.0.0)"] +pyarrow = ["pyarrow (>=7.0.0)"] +pydantic = ["pydantic"] +sqlalchemy = ["pandas", "sqlalchemy"] +timezone = ["backports.zoneinfo", "tzdata"] +xlsx2csv = ["xlsx2csv (>=0.8.0)"] +xlsxwriter = ["xlsxwriter"] + [[package]] name = "prompt-toolkit" version = "3.0.38" @@ -1472,6 +1507,48 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "pyarrow" +version = "13.0.0" +description = "Python library for Apache Arrow" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "pyarrow-13.0.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:1afcc2c33f31f6fb25c92d50a86b7a9f076d38acbcb6f9e74349636109550148"}, + {file = "pyarrow-13.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:70fa38cdc66b2fc1349a082987f2b499d51d072faaa6b600f71931150de2e0e3"}, + {file = "pyarrow-13.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd57b13a6466822498238877892a9b287b0a58c2e81e4bdb0b596dbb151cbb73"}, + {file = "pyarrow-13.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ce69f7bf01de2e2764e14df45b8404fc6f1a5ed9871e8e08a12169f87b7a26"}, + {file = "pyarrow-13.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:588f0d2da6cf1b1680974d63be09a6530fd1bd825dc87f76e162404779a157dc"}, + {file = "pyarrow-13.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6241afd72b628787b4abea39e238e3ff9f34165273fad306c7acf780dd850956"}, + {file = "pyarrow-13.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:fda7857e35993673fcda603c07d43889fca60a5b254052a462653f8656c64f44"}, + {file = "pyarrow-13.0.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:aac0ae0146a9bfa5e12d87dda89d9ef7c57a96210b899459fc2f785303dcbb67"}, + {file = "pyarrow-13.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7759994217c86c161c6a8060509cfdf782b952163569606bb373828afdd82e8"}, + {file = "pyarrow-13.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:868a073fd0ff6468ae7d869b5fc1f54de5c4255b37f44fb890385eb68b68f95d"}, + {file = "pyarrow-13.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51be67e29f3cfcde263a113c28e96aa04362ed8229cb7c6e5f5c719003659d33"}, + {file = "pyarrow-13.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d1b4e7176443d12610874bb84d0060bf080f000ea9ed7c84b2801df851320295"}, + {file = "pyarrow-13.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:69b6f9a089d116a82c3ed819eea8fe67dae6105f0d81eaf0fdd5e60d0c6e0944"}, + {file = "pyarrow-13.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ab1268db81aeb241200e321e220e7cd769762f386f92f61b898352dd27e402ce"}, + {file = "pyarrow-13.0.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:ee7490f0f3f16a6c38f8c680949551053c8194e68de5046e6c288e396dccee80"}, + {file = "pyarrow-13.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3ad79455c197a36eefbd90ad4aa832bece7f830a64396c15c61a0985e337287"}, + {file = "pyarrow-13.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68fcd2dc1b7d9310b29a15949cdd0cb9bc34b6de767aff979ebf546020bf0ba0"}, + {file = "pyarrow-13.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc6fd330fd574c51d10638e63c0d00ab456498fc804c9d01f2a61b9264f2c5b2"}, + {file = "pyarrow-13.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e66442e084979a97bb66939e18f7b8709e4ac5f887e636aba29486ffbf373763"}, + {file = "pyarrow-13.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:0f6eff839a9e40e9c5610d3ff8c5bdd2f10303408312caf4c8003285d0b49565"}, + {file = "pyarrow-13.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b30a27f1cddf5c6efcb67e598d7823a1e253d743d92ac32ec1eb4b6a1417867"}, + {file = "pyarrow-13.0.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:09552dad5cf3de2dc0aba1c7c4b470754c69bd821f5faafc3d774bedc3b04bb7"}, + {file = "pyarrow-13.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3896ae6c205d73ad192d2fc1489cd0edfab9f12867c85b4c277af4d37383c18c"}, + {file = "pyarrow-13.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6647444b21cb5e68b593b970b2a9a07748dd74ea457c7dadaa15fd469c48ada1"}, + {file = "pyarrow-13.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47663efc9c395e31d09c6aacfa860f4473815ad6804311c5433f7085415d62a7"}, + {file = "pyarrow-13.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b9ba6b6d34bd2563345488cf444510588ea42ad5613df3b3509f48eb80250afd"}, + {file = "pyarrow-13.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:d00d374a5625beeb448a7fa23060df79adb596074beb3ddc1838adb647b6ef09"}, + {file = "pyarrow-13.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:c51afd87c35c8331b56f796eff954b9c7f8d4b7fef5903daf4e05fcf017d23a8"}, + {file = "pyarrow-13.0.0.tar.gz", hash = "sha256:83333726e83ed44b0ac94d8d7a21bbdee4a05029c3b1e8db58a863eec8fd8a33"}, +] + +[package.dependencies] +numpy = ">=1.16.6" + [[package]] name = "pycparser" version = "2.21" @@ -2228,8 +2305,9 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more all = ["dash", "plotly"] dash = ["dash"] plotly = ["plotly"] +polars = ["polars", "pyarrow"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "42e881e6ab5ec4ec5664b66f92472349b443622e0a1e6c5cfbdecc18fa016694" +content-hash = "91cce6ad8c7d7f7abe76d404b86f6aa4263bf65fad1973067d89971ff6b22907" diff --git a/pyproject.toml b/pyproject.toml index 7457b4a..43a59d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,10 +20,13 @@ dash = { version = "^2.3.1", optional = true } plotly = { version = "^5.14.1", optional = true } httpx = { extras = ["http2"], version = "^0.24.1" } pytest-asyncio = "^0.21.0" +polars = { version = ">=0.19.3", optional = true } +pyarrow = { version = "^13.0.0", optional = true } [tool.poetry.extras] dash = ["dash"] plotly = ["plotly"] +polars = ["polars", "pyarrow"] all = ["dash", "plotly"] # https://python-poetry.org/docs/managing-dependencies/#dependency-groups diff --git a/subgrounds/contrib/polars/__init__.py b/subgrounds/contrib/polars/__init__.py new file mode 100644 index 0000000..0df4ef8 --- /dev/null +++ b/subgrounds/contrib/polars/__init__.py @@ -0,0 +1,3 @@ +from .client import PolarsSubgrounds + +__all__ = ["PolarsSubgrounds"] diff --git a/subgrounds/contrib/polars/polars_client.py b/subgrounds/contrib/polars/client.py similarity index 90% rename from subgrounds/contrib/polars/polars_client.py rename to subgrounds/contrib/polars/client.py index 18db182..0cb296f 100644 --- a/subgrounds/contrib/polars/polars_client.py +++ b/subgrounds/contrib/polars/client.py @@ -1,33 +1,23 @@ -import functools -import json -import logging import warnings -from collections.abc import Iterator -from contextlib import suppress from functools import cached_property -from typing import Any, Type, cast from json import JSONDecodeError +from typing import Any, Type import httpx from pipe import map, traverse -from subgrounds.dataframe_utils import df_of_json +from subgrounds.client import SubgroundsBase from subgrounds.errors import GraphQLError, ServerError from subgrounds.pagination import LegacyStrategy, PaginationStrategy -from subgrounds.query import DataRequest, DataResponse, Document, DocumentResponse +from subgrounds.query import DataRequest, DataResponse, DocumentResponse from subgrounds.subgraph import FieldPath, Subgraph from subgrounds.utils import default_header -from subgrounds.client import SubgroundsBase - -logger = logging.getLogger("subgrounds") -warnings.simplefilter("default") HTTP2_SUPPORT = True +class PolarsSubgrounds(SubgroundsBase): + """TODO: Write comment""" -class SubgroundsPolars(SubgroundsBase): - # https://github.com/0xPlaygrounds/subgrounds/blob/feat/async/subgrounds/client/sync.py#L48 - # ? what is cached property decorator? 8/28/23 @cached_property def _client(self): """Cached client""" @@ -147,3 +137,10 @@ def query_json( req = self.mk_request(fpaths) data = self.execute(req, pagination_strategy) return [doc.data for doc in data.responses] + + def query( + self, + fpaths: FieldPath | list[FieldPath], + pagination_strategy: Type[PaginationStrategy] | None = LegacyStrategy, + ): + """TODO: Fill in a polars return here.""" diff --git a/subgrounds/contrib/polars/polars_utils.py b/subgrounds/contrib/polars/utils.py similarity index 100% rename from subgrounds/contrib/polars/polars_utils.py rename to subgrounds/contrib/polars/utils.py diff --git a/subgrounds/contrib/polars/test_polars_client.py b/tests/contrib/polars/test_client.py similarity index 100% rename from subgrounds/contrib/polars/test_polars_client.py rename to tests/contrib/polars/test_client.py From a0dfbcb5740f0127dfc4fffd0be33167711040d4 Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Sun, 24 Sep 2023 17:08:45 -0400 Subject: [PATCH 04/14] feat: polars `query_df()` --- subgrounds/contrib/polars/client.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/subgrounds/contrib/polars/client.py b/subgrounds/contrib/polars/client.py index 0cb296f..8a0020c 100644 --- a/subgrounds/contrib/polars/client.py +++ b/subgrounds/contrib/polars/client.py @@ -1,3 +1,4 @@ +import polars as pl import warnings from functools import cached_property from json import JSONDecodeError @@ -7,6 +8,7 @@ from pipe import map, traverse from subgrounds.client import SubgroundsBase +from subgrounds.contrib.polars import utils from subgrounds.errors import GraphQLError, ServerError from subgrounds.pagination import LegacyStrategy, PaginationStrategy from subgrounds.query import DataRequest, DataResponse, DocumentResponse @@ -15,6 +17,7 @@ HTTP2_SUPPORT = True + class PolarsSubgrounds(SubgroundsBase): """TODO: Write comment""" @@ -138,9 +141,24 @@ def query_json( data = self.execute(req, pagination_strategy) return [doc.data for doc in data.responses] - def query( + def query_df( self, fpaths: FieldPath | list[FieldPath], pagination_strategy: Type[PaginationStrategy] | None = LegacyStrategy, - ): - """TODO: Fill in a polars return here.""" + ) -> pl.DataFrame: + """ + `query_df()` queries and converts raw graphql data to a polars dataframe. + """ + + # Query raw graphql data + fpaths = list([fpaths] | traverse | map(FieldPath._auto_select) | traverse) + graphql_data = self.query_json(fpaths, pagination_strategy=pagination_strategy) + + # Get the first key of the first json object. This is the key that contains the data. + json_trades_key = list(graphql_data[0].keys())[0] + + graphql_df = pl.from_dicts( + graphql_data[0][json_trades_key], infer_schema_length=None + ) + + return utils.fmt_dict_cols(graphql_df) From 8f69ceba5faf6d903b04f84d33e13d88d5bbee19 Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Sun, 24 Sep 2023 17:09:07 -0400 Subject: [PATCH 05/14] feat: test `query_df()` --- tests/contrib/polars/test_client.py | 39 +++++------------------------ 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/tests/contrib/polars/test_client.py b/tests/contrib/polars/test_client.py index f421928..1d248ad 100644 --- a/tests/contrib/polars/test_client.py +++ b/tests/contrib/polars/test_client.py @@ -1,11 +1,6 @@ -from polars_client import SubgroundsPolars -from subgrounds.subgraph import FieldPath, Subgraph +from subgrounds.contrib.polars.client import PolarsSubgrounds -from polars_utils import * - -import polars as pl - -sg = SubgroundsPolars() +sg = PolarsSubgrounds() snx_endpoint = "https://api.thegraph.com/subgraphs/name/synthetix-perps/perps" @@ -13,41 +8,19 @@ url=snx_endpoint, ) -trades_json = sg.query_json( + +trades_df = sg.query_df( [ # set the first parameter to a larger size to query more rows. snx.Query.futuresTrades( - first=2500, + first=5000, orderBy="timestamp", orderDirection="desc", - # where=[{"timestamp_lte": "1694131200"}], # 1694131200 = 9/8/23 ) ] ) -json_trades_key = list(trades_json[0].keys())[0] -trades_df = pl.from_dicts( - trades_json[0][list(trades_json[0].keys())[0]], infer_schema_length=None -) - -fmted_df = fmt_dict_cols(trades_df) - -# # polars calculations to convert big ints. All synthetix big ints are represented with 10**18 decimals. -fmted_df = fmted_df.with_columns( - [ - (pl.col("margin") / 10**18), - (pl.col("size") / 10**18), - (pl.col("price") / 10**18), - (pl.col("positionSize") / 10**18), - (pl.col("realizedPnl") / 10**18), - (pl.col("netFunding") / 10**18), - (pl.col("feesPaidToSynthetix") / 10**18), - # convert timestamp to datetime - pl.from_epoch("timestamp").alias("datetime"), - ] -) +print(trades_df.head(10)) -print(fmted_df.shape) -print(fmted_df.head(5)) print("done") From bd221aff0d5efe2a4ddbec15fdc0796858f790d7 Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Sun, 24 Sep 2023 17:11:13 -0400 Subject: [PATCH 06/14] feat: remove unused dependencies --- subgrounds/contrib/polars/utils.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/subgrounds/contrib/polars/utils.py b/subgrounds/contrib/polars/utils.py index b674621..60e4299 100644 --- a/subgrounds/contrib/polars/utils.py +++ b/subgrounds/contrib/polars/utils.py @@ -1,10 +1,4 @@ import polars as pl -import pyarrow as pa - -from time import time -from functools import wraps - -from typing import TYPE_CHECKING, Any ############################ From 1aa273a04d74a921d602cda98fcba514d7755108 Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Sat, 14 Oct 2023 16:59:27 -0400 Subject: [PATCH 07/14] feat: refactor names, comments, and style --- subgrounds/contrib/polars/client.py | 2 +- subgrounds/contrib/polars/utils.py | 68 ++++++++++++++++++++--------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/subgrounds/contrib/polars/client.py b/subgrounds/contrib/polars/client.py index 8a0020c..bccdfc2 100644 --- a/subgrounds/contrib/polars/client.py +++ b/subgrounds/contrib/polars/client.py @@ -161,4 +161,4 @@ def query_df( graphql_data[0][json_trades_key], infer_schema_length=None ) - return utils.fmt_dict_cols(graphql_df) + return utils.format_dictionary_columns(graphql_df) diff --git a/subgrounds/contrib/polars/utils.py b/subgrounds/contrib/polars/utils.py index 60e4299..40cb84e 100644 --- a/subgrounds/contrib/polars/utils.py +++ b/subgrounds/contrib/polars/utils.py @@ -1,44 +1,73 @@ import polars as pl -############################ -# Polars Support Functions - Convert GraphQL Response to Polars DataFrame -############################ +def format_dictionary_columns(df: pl.DataFrame) -> pl.DataFrame: + """ + Unnest dictionary values into their own columns, renaming them appropriately. + Args: + df (pl.DataFrame): Input DataFrame containing dictionary columns. + + Returns: + pl.DataFrame: DataFrame with dictionary values unnested into separate columns. + + Example: + >>> data = { + ... "dict_col": [{"A": 1, "B": 2}, {"A": 3, "B": 4}], + ... "arr_col": [[1, 2, 3], [4, 5, 6]], + ... } + >>> df = pl.DataFrame(data) + >>> result = fmt_dict_cols(df) + >>> print(result) + after test: shape: (2, 3) + ... + (output example here) -def fmt_dict_cols(df: pl.DataFrame) -> pl.DataFrame: - """ - formats dictionary cols, which are 'structs' in a polars df, into separate columns and renames accordingly. """ for column in df.columns: if isinstance(df[column][0], dict): col_names = df[column][0].keys() - # rename struct columns + # Rename struct columns struct_df = df.select( pl.col(column).struct.rename_fields( [f"{column}_{c}" for c in col_names] ) ) struct_df = struct_df.unnest(column) - # add struct_df columns to df and - df = df.with_columns(struct_df) - # drop the df column - df = df.drop(column) - + # Add struct_df columns to df and drop the original column + df = df.with_columns(struct_df).drop(column) return df -def fmt_arr_cols(df: pl.DataFrame) -> pl.DataFrame: +def format_array_columns(df: pl.DataFrame) -> pl.DataFrame: """ - formats lists, which are arrays in a polars df, into separate columns and renames accordingly. - Since there isn't a direct way to convert array -> new columns, we convert the array to a struct and then - unnest the struct into new columns. + Unnest array values into their own columns, renaming them appropriately. + + Args: + df (pl.DataFrame): Input DataFrame containing array columns. + + Returns: + pl.DataFrame: DataFrame with array values unnested into separate columns. + + Example: + >>> data = { + ... "dict_col": [{"A": 1, "B": 2}, {"A": 3, "B": 4}], + ... "arr_col": [[1, 2, 3], [4, 5, 6]], + ... } + >>> df = pl.DataFrame(data) + >>> result = fmt_arr_cols(df) + >>> print(result) + after test: shape: (2, 4) + ... + (output example here) + """ + # use this logic if column is a list (rows show up as pl.Series) for column in df.columns: if isinstance(df[column][0], pl.Series): # convert struct to array - struct_df = df.select([pl.col(column).arr.to_struct()]) + struct_df = df.select([pl.col(column).list.to_struct()]) # rename struct fields struct_df = struct_df.select( pl.col(column).struct.rename_fields( @@ -48,8 +77,5 @@ def fmt_arr_cols(df: pl.DataFrame) -> pl.DataFrame: # unnest struct fields into their own columns struct_df = struct_df.unnest(column) # add struct_df columns to df and - df = df.with_columns(struct_df) - # drop the df column - df = df.drop(column) - + df = df.with_columns(struct_df).drop(column) return df From 5551dbb1a04c01c25be8a37db46ec61e811c86e5 Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Sat, 14 Oct 2023 16:59:42 -0400 Subject: [PATCH 08/14] feat: add functional test for polars utils --- subgrounds/contrib/polars/test_utils.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 subgrounds/contrib/polars/test_utils.py diff --git a/subgrounds/contrib/polars/test_utils.py b/subgrounds/contrib/polars/test_utils.py new file mode 100644 index 0000000..ee84916 --- /dev/null +++ b/subgrounds/contrib/polars/test_utils.py @@ -0,0 +1,20 @@ +from subgrounds.contrib.polars import utils +import polars as pl + + +# prepare test data +test_data = { + "dict_col": [{"A": 1, "B": 2}, {"A": 3, "B": 4}], + "arr_col": [[1, 2, 3], [4, 5, 6]], +} +test_data_df = pl.DataFrame(test_data) + +print(f"before test: {test_data_df}") + +# test fmt_dict_cols() +test_output_df = utils.format_dictionary_columns(test_data_df) +print(f"after test: {test_output_df}") + +# test fmt_arr_cols() +fmt_arr_test_df = utils.format_array_columns(test_data_df) +print(f"after fmt arr test: {fmt_arr_test_df}") From 2ed42f5a7864f442b2a5401430bd35d04143d12e Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Sat, 14 Oct 2023 17:07:40 -0400 Subject: [PATCH 09/14] fix: poetry lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 6eb1779..838511e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2310,4 +2310,4 @@ polars = ["polars", "pyarrow"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "91cce6ad8c7d7f7abe76d404b86f6aa4263bf65fad1973067d89971ff6b22907" +content-hash = "21fc81a15dd3662c4004c2bc8ac16b58bc59b25edfa36b749000c3179c9f7aac" From 5e3288081c78dffc6b1859db0f41ce11a413a76a Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Mon, 16 Oct 2023 20:03:11 -0400 Subject: [PATCH 10/14] chore: update query_df() function style --- subgrounds/contrib/polars/client.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/subgrounds/contrib/polars/client.py b/subgrounds/contrib/polars/client.py index bccdfc2..a0d7c49 100644 --- a/subgrounds/contrib/polars/client.py +++ b/subgrounds/contrib/polars/client.py @@ -147,18 +147,32 @@ def query_df( pagination_strategy: Type[PaginationStrategy] | None = LegacyStrategy, ) -> pl.DataFrame: """ - `query_df()` queries and converts raw graphql data to a polars dataframe. + Queries and converts raw GraphQL data to a Polars DataFrame. + + Args: + fpaths (FieldPath or list[FieldPath]): One or more FieldPath objects that + should be included in the request. + pagination_strategy (Type[PaginationStrategy] or None, optional): + A class implementing the PaginationStrategy Protocol. If None, then automatic + pagination is disabled. Defaults to LegacyStrategy. + + Returns: + pl.DataFrame: A Polars DataFrame containing the queried data. """ - # Query raw graphql data + # Query raw GraphQL data fpaths = list([fpaths] | traverse | map(FieldPath._auto_select) | traverse) graphql_data = self.query_json(fpaths, pagination_strategy=pagination_strategy) - # Get the first key of the first json object. This is the key that contains the data. - json_trades_key = list(graphql_data[0].keys())[0] + # Get the first key of the first JSON object. This is the key that contains the data. + json_data_key = list(graphql_data[0].keys())[0] + # Convert the JSON data to a Polars DataFrame graphql_df = pl.from_dicts( - graphql_data[0][json_trades_key], infer_schema_length=None + graphql_data[0][json_data_key], infer_schema_length=None ) - return utils.format_dictionary_columns(graphql_df) + # Apply the formatting to the Polars DataFrame if necessary + # graphql_df = utils.format_dictionary_columns(graphql_df) + + return graphql_df From 67a0bdb9c0aa57c29970cd9ecaff024073db3c6f Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Wed, 18 Oct 2023 23:23:03 -0400 Subject: [PATCH 11/14] feat: add download param to `query_df()` --- data/curve_swaps.parquet | Bin 0 -> 45625 bytes subgrounds/contrib/polars/client.py | 18 +++++++++++-- subgrounds/contrib/polars/test_download.py | 30 +++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 data/curve_swaps.parquet create mode 100644 subgrounds/contrib/polars/test_download.py diff --git a/data/curve_swaps.parquet b/data/curve_swaps.parquet new file mode 100644 index 0000000000000000000000000000000000000000..e9128556b823747033e1d858eae369a90f0f378d GIT binary patch literal 45625 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 literal 0 HcmV?d00001 diff --git a/subgrounds/contrib/polars/client.py b/subgrounds/contrib/polars/client.py index a0d7c49..f426502 100644 --- a/subgrounds/contrib/polars/client.py +++ b/subgrounds/contrib/polars/client.py @@ -1,3 +1,4 @@ +import os import polars as pl import warnings from functools import cached_property @@ -145,6 +146,7 @@ def query_df( self, fpaths: FieldPath | list[FieldPath], pagination_strategy: Type[PaginationStrategy] | None = LegacyStrategy, + parquet_name: str = None, ) -> pl.DataFrame: """ Queries and converts raw GraphQL data to a Polars DataFrame. @@ -167,12 +169,24 @@ def query_df( # Get the first key of the first JSON object. This is the key that contains the data. json_data_key = list(graphql_data[0].keys())[0] + print("debug statement") + print(graphql_data[0][json_data_key]) # Convert the JSON data to a Polars DataFrame graphql_df = pl.from_dicts( graphql_data[0][json_data_key], infer_schema_length=None ) - # Apply the formatting to the Polars DataFrame if necessary - # graphql_df = utils.format_dictionary_columns(graphql_df) + # Apply the formatting to the Polars DataFrame - can I apply this pre-emptively? + graphql_df = utils.format_dictionary_columns(graphql_df) + graphql_df = utils.format_array_columns(graphql_df) + + match parquet_name: + case None: + pass + case _: + # check if folder exists + os.makedirs("data/", exist_ok=True) + # write to parquet + graphql_df.write_parquet(f"data/{parquet_name}.parquet") return graphql_df diff --git a/subgrounds/contrib/polars/test_download.py b/subgrounds/contrib/polars/test_download.py new file mode 100644 index 0000000..2ac1ad3 --- /dev/null +++ b/subgrounds/contrib/polars/test_download.py @@ -0,0 +1,30 @@ +from subgrounds.contrib.polars.client import PolarsSubgrounds + + +sg = PolarsSubgrounds() + +subgraph = sg.load_subgraph( + "https://api.thegraph.com/subgraphs/name/messari/curve-finance-ethereum" +) + + +# Partial FieldPath selecting the top 4 most traded pools on Curve +curve_swaps = subgraph.Query.swaps( + orderBy=subgraph.Swap.timestamp, + orderDirection="desc", + first=500, +) + +df = sg.query_df( + [ + curve_swaps.timestamp, + curve_swaps.blockNumber, + curve_swaps.pool._select("id"), + curve_swaps.hash, + curve_swaps.tokenIn._select("id"), + curve_swaps.tokenOut._select("id"), + ], + parquet_name="curve_swaps", +) # amountIn and amountOut cols will give int overflow errors. How to deal with this? Maybe just filter. If it isn't timestamp or blockNumber column, then convert to float. + +print(df) From ee1740d9fef8d5d59fa9ede791372ad2acc16706 Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Wed, 18 Oct 2023 23:25:32 -0400 Subject: [PATCH 12/14] chore: refactor, cleanup comments --- subgrounds/contrib/polars/client.py | 4 +--- subgrounds/contrib/polars/test_download.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/subgrounds/contrib/polars/client.py b/subgrounds/contrib/polars/client.py index f426502..e09aa8f 100644 --- a/subgrounds/contrib/polars/client.py +++ b/subgrounds/contrib/polars/client.py @@ -157,7 +157,7 @@ def query_df( pagination_strategy (Type[PaginationStrategy] or None, optional): A class implementing the PaginationStrategy Protocol. If None, then automatic pagination is disabled. Defaults to LegacyStrategy. - + parquet_name (str, optional): The name of the parquet file to write to. Returns: pl.DataFrame: A Polars DataFrame containing the queried data. """ @@ -169,8 +169,6 @@ def query_df( # Get the first key of the first JSON object. This is the key that contains the data. json_data_key = list(graphql_data[0].keys())[0] - print("debug statement") - print(graphql_data[0][json_data_key]) # Convert the JSON data to a Polars DataFrame graphql_df = pl.from_dicts( graphql_data[0][json_data_key], infer_schema_length=None diff --git a/subgrounds/contrib/polars/test_download.py b/subgrounds/contrib/polars/test_download.py index 2ac1ad3..ba6a968 100644 --- a/subgrounds/contrib/polars/test_download.py +++ b/subgrounds/contrib/polars/test_download.py @@ -25,6 +25,6 @@ curve_swaps.tokenOut._select("id"), ], parquet_name="curve_swaps", -) # amountIn and amountOut cols will give int overflow errors. How to deal with this? Maybe just filter. If it isn't timestamp or blockNumber column, then convert to float. +) print(df) From dd7bd3897225326d3a8d8cbcbbe7d5a5ca106a6f Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Wed, 18 Oct 2023 23:44:12 -0400 Subject: [PATCH 13/14] feat: convert float to int --- .../contrib/polars/test_force_numeric.py | 28 +++++++++++++++++++ subgrounds/contrib/polars/utils.py | 12 ++++++++ 2 files changed, 40 insertions(+) create mode 100644 subgrounds/contrib/polars/test_force_numeric.py diff --git a/subgrounds/contrib/polars/test_force_numeric.py b/subgrounds/contrib/polars/test_force_numeric.py new file mode 100644 index 0000000..e8a4c02 --- /dev/null +++ b/subgrounds/contrib/polars/test_force_numeric.py @@ -0,0 +1,28 @@ +from subgrounds.contrib.polars.utils import ( + force_numeric, +) + +overflow_data = [ + { + "x24e88aa9a8dbf48e": [ + { + "blockNumber": 18381720, + "amountIn": 1008310, + "amountOut": 10082717795683768903291, + "id": "swap-0xb67a23794697275f14c0b4e3d6d73960d13a6df4e0e1d3cd2269a9bbffe59d3e-49", + "timestamp": 1697686283, + }, + { + "blockNumber": 18381710, + "amountIn": 2402410386963680919, + "amountOut": 7683170744157548213, + "id": "swap-0x37fede110a1267df8833ad2b0db6f85d663e6b30cd1b85757314c67e6235b5e6-69", + "timestamp": 1697686163, + }, + ] + } +] + +output = force_numeric(overflow_data[0]["x24e88aa9a8dbf48e"]) + +print(output) diff --git a/subgrounds/contrib/polars/utils.py b/subgrounds/contrib/polars/utils.py index 40cb84e..2219b36 100644 --- a/subgrounds/contrib/polars/utils.py +++ b/subgrounds/contrib/polars/utils.py @@ -79,3 +79,15 @@ def format_array_columns(df: pl.DataFrame) -> pl.DataFrame: # add struct_df columns to df and df = df.with_columns(struct_df).drop(column) return df + + +def force_numeric(json_data: list[str]) -> list[str]: + # scan all keys. If one of the keys is timestamp or blockNumber, then leave alone. For any other key that has int values, convert to float + # print(json_data) + + for entry in json_data: + for key, value in entry.items(): + if key != "timestamp" and key != "blockNumber" and isinstance(value, int): + entry[key] = float(value) + + return json_data From ee3a5b013753b40e3b9881c0d617bffbf00e363d Mon Sep 17 00:00:00 2001 From: Evan-Kim2028 Date: Wed, 18 Oct 2023 23:44:26 -0400 Subject: [PATCH 14/14] feat: test for int overflow --- subgrounds/contrib/polars/client.py | 10 ++++--- .../contrib/polars/test_int_overflow.py | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 subgrounds/contrib/polars/test_int_overflow.py diff --git a/subgrounds/contrib/polars/client.py b/subgrounds/contrib/polars/client.py index e09aa8f..0274a64 100644 --- a/subgrounds/contrib/polars/client.py +++ b/subgrounds/contrib/polars/client.py @@ -15,6 +15,7 @@ from subgrounds.query import DataRequest, DataResponse, DocumentResponse from subgrounds.subgraph import FieldPath, Subgraph from subgrounds.utils import default_header +from subgrounds.contrib.polars.utils import force_numeric HTTP2_SUPPORT = True @@ -168,11 +169,14 @@ def query_df( # Get the first key of the first JSON object. This is the key that contains the data. json_data_key = list(graphql_data[0].keys())[0] + numeric_data = force_numeric(graphql_data[0][json_data_key]) # Convert the JSON data to a Polars DataFrame - graphql_df = pl.from_dicts( - graphql_data[0][json_data_key], infer_schema_length=None - ) + # graphql_df = pl.from_dicts( + # graphql_data[0][json_data_key], infer_schema_length=None + # ) + + graphql_df = pl.from_dicts(numeric_data, infer_schema_length=None) # Apply the formatting to the Polars DataFrame - can I apply this pre-emptively? graphql_df = utils.format_dictionary_columns(graphql_df) diff --git a/subgrounds/contrib/polars/test_int_overflow.py b/subgrounds/contrib/polars/test_int_overflow.py new file mode 100644 index 0000000..2285b7d --- /dev/null +++ b/subgrounds/contrib/polars/test_int_overflow.py @@ -0,0 +1,27 @@ +from subgrounds.contrib.polars.client import PolarsSubgrounds + + +sg = PolarsSubgrounds() + +subgraph = sg.load_subgraph( + "https://api.thegraph.com/subgraphs/name/messari/curve-finance-ethereum" +) + + +# Partial FieldPath selecting the top 4 most traded pools on Curve +curve_swaps = subgraph.Query.swaps( + orderBy=subgraph.Swap.timestamp, + orderDirection="desc", + first=100, +) + +df = sg.query_df( + [ + curve_swaps.timestamp, + curve_swaps.blockNumber, + curve_swaps.amountIn, + curve_swaps.amountOut, + ] +) # amountIn and amountOut cols will give int overflow errors. How to deal with this? Maybe just filter. If it isn't timestamp or blockNumber column, then convert to float. + +print(df)