From 77ea93586795cae56c9537a05169da207ad56edd Mon Sep 17 00:00:00 2001 From: Erez Shinan Date: Wed, 22 Jun 2022 14:58:19 +0200 Subject: [PATCH 1/2] Move common ABCs and types to database_types.py; Fix type annotations --- data_diff/database.py | 198 +++++------------------------------ data_diff/database_types.py | 200 ++++++++++++++++++++++++++++++++++++ data_diff/diff_tables.py | 54 ++-------- data_diff/sql.py | 21 ++-- pyproject.toml | 2 +- tests/test_api.py | 2 +- 6 files changed, 246 insertions(+), 231 deletions(-) create mode 100644 data_diff/database_types.py diff --git a/data_diff/database.py b/data_diff/database.py index b89c2106..01a0850a 100644 --- a/data_diff/database.py +++ b/data_diff/database.py @@ -3,17 +3,18 @@ from itertools import zip_longest import re from abc import ABC, abstractmethod -from runtype import dataclass import logging -from typing import Sequence, Tuple, Optional, List +from typing import Sequence, Tuple, Optional, List, Type from concurrent.futures import ThreadPoolExecutor import threading from typing import Dict - import dsnparse import sys +from runtype import dataclass + from .sql import DbPath, SqlOrStr, Compiler, Explain, Select +from .database_types import * logger = logging.getLogger("database") @@ -109,149 +110,6 @@ def _query_conn(conn, sql_code: str) -> list: return c.fetchall() -class ColType: - pass - - -@dataclass -class PrecisionType(ColType): - precision: Optional[int] - rounds: bool - - -class TemporalType(PrecisionType): - pass - - -class Timestamp(TemporalType): - pass - - -class TimestampTZ(TemporalType): - pass - - -class Datetime(TemporalType): - pass - - -@dataclass -class NumericType(ColType): - # 'precision' signifies how many fractional digits (after the dot) we want to compare - precision: int - - -class Float(NumericType): - pass - - -class Decimal(NumericType): - pass - - -@dataclass -class Integer(Decimal): - def __post_init__(self): - assert self.precision == 0 - - -@dataclass -class UnknownColType(ColType): - text: str - - -class AbstractDatabase(ABC): - @abstractmethod - def quote(self, s: str): - "Quote SQL name (implementation specific)" - ... - - @abstractmethod - def to_string(self, s: str) -> str: - "Provide SQL for casting a column to string" - ... - - @abstractmethod - def md5_to_int(self, s: str) -> str: - "Provide SQL for computing md5 and returning an int" - ... - - @abstractmethod - def _query(self, sql_code: str) -> list: - "Send query to database and return result" - ... - - @abstractmethod - def select_table_schema(self, path: DbPath) -> str: - "Provide SQL for selecting the table schema as (name, type, date_prec, num_prec)" - ... - - @abstractmethod - def query_table_schema(self, path: DbPath, filter_columns: Optional[Sequence[str]] = None) -> Dict[str, ColType]: - "Query the table for its schema for table in 'path', and return {column: type}" - ... - - @abstractmethod - def parse_table_name(self, name: str) -> DbPath: - "Parse the given table name into a DbPath" - ... - - @abstractmethod - def close(self): - "Close connection(s) to the database instance. Querying will stop functioning." - ... - - @abstractmethod - def normalize_timestamp(self, value: str, coltype: ColType) -> str: - """Creates an SQL expression, that converts 'value' to a normalized timestamp. - - The returned expression must accept any SQL datetime/timestamp, and return a string. - - Date format: "YYYY-MM-DD HH:mm:SS.FFFFFF" - - Precision of dates should be rounded up/down according to coltype.rounds - """ - ... - - @abstractmethod - def normalize_number(self, value: str, coltype: ColType) -> str: - """Creates an SQL expression, that converts 'value' to a normalized number. - - The returned expression must accept any SQL int/numeric/float, and return a string. - - - Floats/Decimals are expected in the format - "I.P" - - Where I is the integer part of the number (as many digits as necessary), - and must be at least one digit (0). - P is the fractional digits, the amount of which is specified with - coltype.precision. Trailing zeroes may be necessary. - If P is 0, the dot is omitted. - - Note: This precision is different than the one used by databases. For decimals, - it's the same as ``numeric_scale``, and for floats, who use binary precision, - it can be calculated as ``log10(2**numeric_precision)``. - """ - ... - - def normalize_value_by_type(self, value: str, coltype: ColType) -> str: - """Creates an SQL expression, that converts 'value' to a normalized representation. - - The returned expression must accept any SQL value, and return a string. - - The default implementation dispatches to a method according to ``coltype``: - - TemporalType -> normalize_timestamp() - NumericType -> normalize_number() - -else- -> to_string() - - """ - if isinstance(coltype, TemporalType): - return self.normalize_timestamp(value, coltype) - elif isinstance(coltype, NumericType): - return self.normalize_number(value, coltype) - return self.to_string(f"{value}") - class Database(AbstractDatabase): """Base abstract class for databases. @@ -261,8 +119,8 @@ class Database(AbstractDatabase): Instanciated using :meth:`~data_diff.connect_to_uri` """ - DATETIME_TYPES = {} - default_schema = None + DATETIME_TYPES: Dict[str, type] = {} + default_schema: str = None @property def name(self): @@ -412,9 +270,6 @@ def _query_in_worker(self, sql_code: str): raise self._init_error return _query_conn(self.thread_local.conn, sql_code) - def close(self): - self._queue.shutdown(True) - @abstractmethod def create_connection(self): ... @@ -481,7 +336,7 @@ def md5_to_int(self, s: str) -> str: def to_string(self, s: str): return f"{s}::varchar" - def normalize_timestamp(self, value: str, coltype: ColType) -> str: + def normalize_timestamp(self, value: str, coltype: TemporalType) -> str: if coltype.rounds: return f"to_char({value}::timestamp({coltype.precision}), 'YYYY-mm-dd HH24:MI:SS.US')" @@ -490,7 +345,7 @@ def normalize_timestamp(self, value: str, coltype: ColType) -> str: f"RPAD(LEFT({timestamp6}, {TIMESTAMP_PRECISION_POS+coltype.precision}), {TIMESTAMP_PRECISION_POS+6}, '0')" ) - def normalize_number(self, value: str, coltype: ColType) -> str: + def normalize_number(self, value: str, coltype: NumericType) -> str: return self.to_string(f"{value}::decimal(38, {coltype.precision})") @@ -531,7 +386,7 @@ def _query(self, sql_code: str) -> list: def close(self): self._conn.close() - def normalize_timestamp(self, value: str, coltype: ColType) -> str: + def normalize_timestamp(self, value: str, coltype: TemporalType) -> str: # TODO if coltype.rounds: s = f"date_format(cast({value} as timestamp(6)), '%Y-%m-%d %H:%i:%S.%f')" @@ -540,7 +395,7 @@ def normalize_timestamp(self, value: str, coltype: ColType) -> str: return f"RPAD(RPAD({s}, {TIMESTAMP_PRECISION_POS+coltype.precision}, '.'), {TIMESTAMP_PRECISION_POS+6}, '0')" - def normalize_number(self, value: str, coltype: ColType) -> str: + def normalize_number(self, value: str, coltype: NumericType) -> str: return self.to_string(f"cast({value} as decimal(38,{coltype.precision}))") def select_table_schema(self, path: DbPath) -> str: @@ -554,11 +409,11 @@ def select_table_schema(self, path: DbPath) -> str: def _parse_type( self, col_name: str, type_repr: str, datetime_precision: int = None, numeric_precision: int = None ) -> ColType: - regexps = { + timestamp_regexps = { r"timestamp\((\d)\)": Timestamp, r"timestamp\((\d)\) with time zone": TimestampTZ, } - for regexp, cls in regexps.items(): + for regexp, cls in timestamp_regexps.items(): m = re.match(regexp + "$", type_repr) if m: datetime_precision = int(m.group(1)) @@ -567,8 +422,8 @@ def _parse_type( rounds=False, ) - regexps = {r"decimal\((\d+),(\d+)\)": Decimal} - for regexp, cls in regexps.items(): + number_regexps = {r"decimal\((\d+),(\d+)\)": Decimal} + for regexp, cls in number_regexps.items(): m = re.match(regexp + "$", type_repr) if m: prec, scale = map(int, m.groups()) @@ -632,14 +487,14 @@ def md5_to_int(self, s: str) -> str: def to_string(self, s: str): return f"cast({s} as char)" - def normalize_timestamp(self, value: str, coltype: ColType) -> str: + def normalize_timestamp(self, value: str, coltype: TemporalType) -> str: if coltype.rounds: return self.to_string(f"cast( cast({value} as datetime({coltype.precision})) as datetime(6))") s = self.to_string(f"cast({value} as datetime(6))") return f"RPAD(RPAD({s}, {TIMESTAMP_PRECISION_POS+coltype.precision}, '.'), {TIMESTAMP_PRECISION_POS+6}, '0')" - def normalize_number(self, value: str, coltype: ColType) -> str: + def normalize_number(self, value: str, coltype: NumericType) -> str: return self.to_string(f"cast({value} as decimal(38, {coltype.precision}))") @@ -685,10 +540,10 @@ def select_table_schema(self, path: DbPath) -> str: f" FROM USER_TAB_COLUMNS WHERE table_name = '{table.upper()}'" ) - def normalize_timestamp(self, value: str, coltype: ColType) -> str: + def normalize_timestamp(self, value: str, coltype: TemporalType) -> str: return f"to_char(cast({value} as timestamp({coltype.precision})), 'YYYY-MM-DD HH24:MI:SS.FF6')" - def normalize_number(self, value: str, coltype: ColType) -> str: + def normalize_number(self, value: str, coltype: NumericType) -> str: # FM999.9990 format_str = "FM" + "9" * (38 - coltype.precision) if coltype.precision: @@ -749,7 +604,7 @@ class Redshift(PostgreSQL): def md5_to_int(self, s: str) -> str: return f"strtol(substring(md5({s}), {1+MD5_HEXDIGITS-CHECKSUM_HEXDIGITS}), 16)::decimal(38)" - def normalize_timestamp(self, value: str, coltype: ColType) -> str: + def normalize_timestamp(self, value: str, coltype: TemporalType) -> str: if coltype.rounds: timestamp = f"{value}::timestamp(6)" # Get seconds since epoch. Redshift doesn't support milli- or micro-seconds. @@ -769,7 +624,7 @@ def normalize_timestamp(self, value: str, coltype: ColType) -> str: f"RPAD(LEFT({timestamp6}, {TIMESTAMP_PRECISION_POS+coltype.precision}), {TIMESTAMP_PRECISION_POS+6}, '0')" ) - def normalize_number(self, value: str, coltype: ColType) -> str: + def normalize_number(self, value: str, coltype: NumericType) -> str: return self.to_string(f"{value}::decimal(38,{coltype.precision})") def select_table_schema(self, path: DbPath) -> str: @@ -870,7 +725,7 @@ def select_table_schema(self, path: DbPath) -> str: f"WHERE table_name = '{table}' AND table_schema = '{schema}'" ) - def normalize_timestamp(self, value: str, coltype: ColType) -> str: + def normalize_timestamp(self, value: str, coltype: TemporalType) -> str: if coltype.rounds: timestamp = f"timestamp_micros(cast(round(unix_micros(cast({value} as timestamp))/1000000, {coltype.precision})*1000000 as int))" return f"FORMAT_TIMESTAMP('%F %H:%M:%E6S', {timestamp})" @@ -885,7 +740,7 @@ def normalize_timestamp(self, value: str, coltype: ColType) -> str: f"RPAD(LEFT({timestamp6}, {TIMESTAMP_PRECISION_POS+coltype.precision}), {TIMESTAMP_PRECISION_POS+6}, '0')" ) - def normalize_number(self, value: str, coltype: ColType) -> str: + def normalize_number(self, value: str, coltype: NumericType) -> str: if isinstance(coltype, Integer): return self.to_string(value) return f"format('%.{coltype.precision}f', {value})" @@ -962,7 +817,7 @@ def select_table_schema(self, path: DbPath) -> str: schema, table = self._normalize_table_path(path) return super().select_table_schema((schema, table)) - def normalize_timestamp(self, value: str, coltype: ColType) -> str: + def normalize_timestamp(self, value: str, coltype: TemporalType) -> str: if coltype.rounds: timestamp = f"to_timestamp(round(date_part(epoch_nanosecond, {value}::timestamp(9))/1000000000, {coltype.precision}))" else: @@ -970,13 +825,13 @@ def normalize_timestamp(self, value: str, coltype: ColType) -> str: return f"to_char({timestamp}, 'YYYY-MM-DD HH24:MI:SS.FF6')" - def normalize_number(self, value: str, coltype: ColType) -> str: + def normalize_number(self, value: str, coltype: NumericType) -> str: return self.to_string(f"cast({value} as decimal(38, {coltype.precision}))") @dataclass class MatchUriPath: - database_cls: type + database_cls: Type[Database] params: List[str] kwparams: List[str] = [] help_str: str @@ -1027,7 +882,7 @@ def match_path(self, dsn): "postgresql": MatchUriPath(PostgreSQL, ["database?"], help_str="postgresql://:@/"), "mysql": MatchUriPath(MySQL, ["database?"], help_str="mysql://:@/"), "oracle": MatchUriPath(Oracle, ["database?"], help_str="oracle://:@/"), - "mssql": MatchUriPath(MsSQL, ["database?"], help_str="mssql://:@/"), + # "mssql": MatchUriPath(MsSQL, ["database?"], help_str="mssql://:@/"), "redshift": MatchUriPath(Redshift, ["database?"], help_str="redshift://:@/"), "snowflake": MatchUriPath( Snowflake, @@ -1055,7 +910,6 @@ def connect_to_uri(db_uri: str, thread_count: Optional[int] = 1) -> Database: Supported schemes: - postgresql - mysql - - mssql - oracle - snowflake - bigquery diff --git a/data_diff/database_types.py b/data_diff/database_types.py new file mode 100644 index 00000000..c441c4cb --- /dev/null +++ b/data_diff/database_types.py @@ -0,0 +1,200 @@ +from abc import ABC, abstractmethod +from typing import Sequence, Optional, Tuple, Union, Dict +from datetime import datetime + +from runtype import dataclass + +DbPath = Tuple[str, ...] +DbKey = Union[int, str, bytes] +DbTime = datetime + + +class ColType: + pass + + +@dataclass +class PrecisionType(ColType): + precision: int + rounds: bool + + +class TemporalType(PrecisionType): + pass + + +class Timestamp(TemporalType): + pass + + +class TimestampTZ(TemporalType): + pass + + +class Datetime(TemporalType): + pass + + +@dataclass +class NumericType(ColType): + # 'precision' signifies how many fractional digits (after the dot) we want to compare + precision: int + + +class Float(NumericType): + pass + + +class Decimal(NumericType): + pass + + +@dataclass +class Integer(Decimal): + def __post_init__(self): + assert self.precision == 0 + + +@dataclass +class UnknownColType(ColType): + text: str + + +class AbstractDatabase(ABC): + @abstractmethod + def quote(self, s: str): + "Quote SQL name (implementation specific)" + ... + + @abstractmethod + def to_string(self, s: str) -> str: + "Provide SQL for casting a column to string" + ... + + @abstractmethod + def md5_to_int(self, s: str) -> str: + "Provide SQL for computing md5 and returning an int" + ... + + @abstractmethod + def _query(self, sql_code: str) -> list: + "Send query to database and return result" + ... + + @abstractmethod + def select_table_schema(self, path: DbPath) -> str: + "Provide SQL for selecting the table schema as (name, type, date_prec, num_prec)" + ... + + @abstractmethod + def query_table_schema(self, path: DbPath, filter_columns: Optional[Sequence[str]] = None) -> Dict[str, ColType]: + "Query the table for its schema for table in 'path', and return {column: type}" + ... + + @abstractmethod + def parse_table_name(self, name: str) -> DbPath: + "Parse the given table name into a DbPath" + ... + + @abstractmethod + def close(self): + "Close connection(s) to the database instance. Querying will stop functioning." + ... + + @abstractmethod + def normalize_timestamp(self, value: str, coltype: TemporalType) -> str: + """Creates an SQL expression, that converts 'value' to a normalized timestamp. + + The returned expression must accept any SQL datetime/timestamp, and return a string. + + Date format: "YYYY-MM-DD HH:mm:SS.FFFFFF" + + Precision of dates should be rounded up/down according to coltype.rounds + """ + ... + + @abstractmethod + def normalize_number(self, value: str, coltype: NumericType) -> str: + """Creates an SQL expression, that converts 'value' to a normalized number. + + The returned expression must accept any SQL int/numeric/float, and return a string. + + - Floats/Decimals are expected in the format + "I.P" + + Where I is the integer part of the number (as many digits as necessary), + and must be at least one digit (0). + P is the fractional digits, the amount of which is specified with + coltype.precision. Trailing zeroes may be necessary. + If P is 0, the dot is omitted. + + Note: This precision is different than the one used by databases. For decimals, + it's the same as ``numeric_scale``, and for floats, who use binary precision, + it can be calculated as ``log10(2**numeric_precision)``. + """ + ... + + def normalize_value_by_type(self, value: str, coltype: ColType) -> str: + """Creates an SQL expression, that converts 'value' to a normalized representation. + + The returned expression must accept any SQL value, and return a string. + + The default implementation dispatches to a method according to ``coltype``: + + TemporalType -> normalize_timestamp() + NumericType -> normalize_number() + -else- -> to_string() + + """ + if isinstance(coltype, TemporalType): + return self.normalize_timestamp(value, coltype) + elif isinstance(coltype, NumericType): + return self.normalize_number(value, coltype) + return self.to_string(f"{value}") + + + def _normalize_table_path(self, path: DbPath) -> DbPath: + ... + + +class Schema(ABC): + @abstractmethod + def get_key(self, key: str) -> str: + ... + + @abstractmethod + def __getitem__(self, key: str) -> ColType: + ... + + @abstractmethod + def __setitem__(self, key: str, value): + ... + + @abstractmethod + def __contains__(self, key: str) -> bool: + ... + + +class Schema_CaseSensitive(dict, Schema): + def get_key(self, key): + return key + + +class Schema_CaseInsensitive(Schema): + def __init__(self, initial): + self._dict = {k.lower(): (k, v) for k, v in dict(initial).items()} + + def get_key(self, key: str) -> str: + return self._dict[key.lower()][0] + + def __getitem__(self, key: str) -> ColType: + return self._dict[key.lower()][1] + + def __setitem__(self, key: str, value): + k = key.lower() + if k in self._dict: + key = self._dict[k][0] + self._dict[k] = key, value + + def __contains__(self, key): + return key.lower() in self._dict diff --git a/data_diff/diff_tables.py b/data_diff/diff_tables.py index aee65cca..b5544927 100644 --- a/data_diff/diff_tables.py +++ b/data_diff/diff_tables.py @@ -12,7 +12,8 @@ from runtype import dataclass from .sql import Select, Checksum, Compare, DbPath, DbKey, DbTime, Count, TableName, Time, Min, Max -from .database import Database, NumericType, PrecisionType, ColType, UnknownColType +from .database import Database +from .database_types import NumericType, PrecisionType, UnknownColType, Schema, Schema_CaseInsensitive, Schema_CaseSensitive logger = logging.getLogger("diff_tables") @@ -37,48 +38,6 @@ def parse_table_name(t): return tuple(t.split(".")) -class Schema(ABC): - @abstractmethod - def get_key(self, key: str) -> str: - ... - - @abstractmethod - def __getitem__(self, key: str) -> str: - ... - - @abstractmethod - def __setitem__(self, key: str, value): - ... - - @abstractmethod - def __contains__(self, key: str) -> bool: - ... - - -class Schema_CaseSensitive(dict, Schema): - def get_key(self, key): - return key - - -class Schema_CaseInsensitive(Schema): - def __init__(self, initial): - self._dict = {k.lower(): (k, v) for k, v in dict(initial).items()} - - def get_key(self, key: str) -> str: - return self._dict[key.lower()][0] - - def __getitem__(self, key: str) -> str: - return self._dict[key.lower()][1] - - def __setitem__(self, key: str, value): - k = key.lower() - if k in self._dict: - key = self._dict[k][0] - self._dict[k] = key, value - - def __contains__(self, key): - return key.lower() in self._dict - @dataclass(frozen=False) class TableSegment: @@ -144,16 +103,19 @@ def with_schema(self) -> "TableSegment": return self schema = self.database.query_table_schema(self.table_path, self._relevant_columns) + + schema_inst: Schema if self.case_sensitive: - schema = Schema_CaseSensitive(schema) + schema_inst = Schema_CaseSensitive(schema) else: if len({k.lower() for k in schema}) < len(schema): logger.warning( f'Ambiguous schema for {self.database}:{".".join(self.table_path)} | Columns = {", ".join(list(schema))}' ) logger.warning("We recommend to disable case-insensitivity (remove --any-case).") - schema = Schema_CaseInsensitive(schema) - return self.new(_schema=schema) + schema_inst = Schema_CaseInsensitive(schema) + + return self.new(_schema=schema_inst) def _make_key_range(self): if self.min_key is not None: diff --git a/data_diff/sql.py b/data_diff/sql.py index a81839eb..455aec85 100644 --- a/data_diff/sql.py +++ b/data_diff/sql.py @@ -1,14 +1,13 @@ """Provides classes for a pseudo-SQL AST that compiles to SQL code """ -from typing import List, Union, Tuple, Optional +from typing import List, Sequence, Union, Tuple, Optional from datetime import datetime from runtype import dataclass -DbPath = Tuple[str, ...] -DbKey = Union[int, str, bytes] -DbTime = datetime +from .database_types import AbstractDatabase, DbPath, DbKey, DbTime + class Sql: @@ -25,7 +24,7 @@ class Compiler: For internal use. """ - database: object # Database + database: AbstractDatabase in_select: bool = False # Compilation def quote(self, s: str): @@ -72,11 +71,11 @@ def compile(self, c: Compiler): @dataclass class Select(Sql): - columns: List[SqlOrStr] + columns: Sequence[SqlOrStr] table: SqlOrStr = None - where: List[SqlOrStr] = None - order_by: List[SqlOrStr] = None - group_by: List[SqlOrStr] = None + where: Sequence[SqlOrStr] = None + order_by: Sequence[SqlOrStr] = None + group_by: Sequence[SqlOrStr] = None def compile(self, parent_c: Compiler): c = parent_c.replace(in_select=True) @@ -113,7 +112,7 @@ def compile(self, c: Compiler): @dataclass class Checksum(Sql): - exprs: List[SqlOrStr] + exprs: Sequence[SqlOrStr] def compile(self, c: Compiler): compiled_exprs = ", ".join(map(c.compile, self.exprs)) @@ -135,7 +134,7 @@ def compile(self, c: Compiler): @dataclass class In(Sql): expr: SqlOrStr - list: List # List[SqlOrStr] + list: Sequence # List[SqlOrStr] def compile(self, c: Compiler): elems = ", ".join(map(c.compile, self.list)) diff --git a/pyproject.toml b/pyproject.toml index 11c697b1..e2f4c0ca 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ packages = [{ include = "data_diff" }] [tool.poetry.dependencies] python = "^3.7" -runtype = "^0.2.4" +runtype = "^0.2.6" dsnparse = "*" click = "^8.1" rich = "^10.16.2" diff --git a/tests/test_api.py b/tests/test_api.py index 2a532edd..e9e56f67 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -15,7 +15,7 @@ def setUpClass(cls): cls.preql = preql.Preql(TEST_MYSQL_CONN_STRING) def setUp(self) -> None: - self.preql = preql.Preql(TEST_MYSQL_CONN_STRING) + # self.preql = preql.Preql(TEST_MYSQL_CONN_STRING) self.preql( r""" table test_api { From 480762794044a8720b96b2a270da0a61e0bb5f9f Mon Sep 17 00:00:00 2001 From: Erez Shinan Date: Wed, 22 Jun 2022 15:43:42 +0200 Subject: [PATCH 2/2] Update lock --- poetry.lock | 61 ++++++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7172ba56..b01ec1b5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -260,7 +260,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycryptodomex" -version = "3.14.1" +version = "3.15.0" description = "Cryptographic library for Python" category = "main" optional = false @@ -452,7 +452,7 @@ snowflake = ["snowflake-connector-python"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "e1b2b05a166d2d6d81bec8e15e562480998b6e578592a4a0ed04b6fb6a2e046c" +content-hash = "7300fb766abdc38cf5ce57d26f9ca026cee3a26e0b976d6bf5a2a736c36744e5" [metadata.files] arrow = [ @@ -645,33 +645,36 @@ pycparser = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pycryptodomex = [ - {file = "pycryptodomex-3.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca88f2f7020002638276439a01ffbb0355634907d1aa5ca91f3dc0c2e44e8f3b"}, - {file = "pycryptodomex-3.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:8536bc08d130cae6dcba1ea689f2913dfd332d06113904d171f2f56da6228e89"}, - {file = "pycryptodomex-3.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:406ec8cfe0c098fadb18d597dc2ee6de4428d640c0ccafa453f3d9b2e58d29e2"}, - {file = "pycryptodomex-3.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:da8db8374295fb532b4b0c467e66800ef17d100e4d5faa2bbbd6df35502da125"}, - {file = "pycryptodomex-3.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:d709572d64825d8d59ea112e11cc7faf6007f294e9951324b7574af4251e4de8"}, - {file = "pycryptodomex-3.14.1-cp27-cp27m-win32.whl", hash = "sha256:3da13c2535b7aea94cc2a6d1b1b37746814c74b6e80790daddd55ca5c120a489"}, - {file = "pycryptodomex-3.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:298c00ea41a81a491d5b244d295d18369e5aac4b61b77b2de5b249ca61cd6659"}, - {file = "pycryptodomex-3.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:77931df40bb5ce5e13f4de2bfc982b2ddc0198971fbd947776c8bb5050896eb2"}, - {file = "pycryptodomex-3.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:c5dd3ffa663c982d7f1be9eb494a8924f6d40e2e2f7d1d27384cfab1b2ac0662"}, - {file = "pycryptodomex-3.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2aa887683eee493e015545bd69d3d21ac8d5ad582674ec98f4af84511e353e45"}, - {file = "pycryptodomex-3.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:8085bd0ad2034352eee4d4f3e2da985c2749cb7344b939f4d95ead38c2520859"}, - {file = "pycryptodomex-3.14.1-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e95a4a6c54d27a84a4624d2af8bb9ee178111604653194ca6880c98dcad92f48"}, - {file = "pycryptodomex-3.14.1-cp35-abi3-manylinux1_i686.whl", hash = "sha256:a4d412eba5679ede84b41dbe48b1bed8f33131ab9db06c238a235334733acc5e"}, - {file = "pycryptodomex-3.14.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:d2cce1c82a7845d7e2e8a0956c6b7ed3f1661c9acf18eb120fc71e098ab5c6fe"}, - {file = "pycryptodomex-3.14.1-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:f75009715dcf4a3d680c2338ab19dac5498f8121173a929872950f4fb3a48fbf"}, - {file = "pycryptodomex-3.14.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:1ca8e1b4c62038bb2da55451385246f51f412c5f5eabd64812c01766a5989b4a"}, - {file = "pycryptodomex-3.14.1-cp35-abi3-win32.whl", hash = "sha256:ee835def05622e0c8b1435a906491760a43d0c462f065ec9143ec4b8d79f8bff"}, - {file = "pycryptodomex-3.14.1-cp35-abi3-win_amd64.whl", hash = "sha256:b5a185ae79f899b01ca49f365bdf15a45d78d9856f09b0de1a41b92afce1a07f"}, - {file = "pycryptodomex-3.14.1-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:797a36bd1f69df9e2798e33edb4bd04e5a30478efc08f9428c087f17f65a7045"}, - {file = "pycryptodomex-3.14.1-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:aebecde2adc4a6847094d3bd6a8a9538ef3438a5ea84ac1983fcb167db614461"}, - {file = "pycryptodomex-3.14.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:f8524b8bc89470cec7ac51734907818d3620fb1637f8f8b542d650ebec42a126"}, - {file = "pycryptodomex-3.14.1-pp27-pypy_73-win32.whl", hash = "sha256:4d0db8df9ffae36f416897ad184608d9d7a8c2b46c4612c6bc759b26c073f750"}, - {file = "pycryptodomex-3.14.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b276cc4deb4a80f9dfd47a41ebb464b1fe91efd8b1b8620cf5ccf8b824b850d6"}, - {file = "pycryptodomex-3.14.1-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:e36c7e3b5382cd5669cf199c4a04a0279a43b2a3bdd77627e9b89778ac9ec08c"}, - {file = "pycryptodomex-3.14.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c4d8977ccda886d88dc3ca789de2f1adc714df912ff3934b3d0a3f3d777deafb"}, - {file = "pycryptodomex-3.14.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:530756d2faa40af4c1f74123e1d889bd07feae45bac2fd32f259a35f7aa74151"}, - {file = "pycryptodomex-3.14.1.tar.gz", hash = "sha256:2ce76ed0081fd6ac8c74edc75b9d14eca2064173af79843c24fa62573263c1f2"}, + {file = "pycryptodomex-3.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:6f5b6ba8aefd624834bc177a2ac292734996bb030f9d1b388e7504103b6fcddf"}, + {file = "pycryptodomex-3.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4540904c09704b6f831059c0dfb38584acb82cb97b0125cd52688c1f1e3fffa6"}, + {file = "pycryptodomex-3.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0fadb9f7fa3150577800eef35f62a8a24b9ddf1563ff060d9bd3af22d3952c8c"}, + {file = "pycryptodomex-3.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:fc9bc7a9b79fe5c750fc81a307052f8daabb709bdaabb0fb18fb136b66b653b5"}, + {file = "pycryptodomex-3.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f8be976cec59b11f011f790b88aca67b4ea2bd286578d0bd3e31bcd19afcd3e4"}, + {file = "pycryptodomex-3.15.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:78d9621cf0ea35abf2d38fa2ca6d0634eab6c991a78373498ab149953787e5e5"}, + {file = "pycryptodomex-3.15.0-cp27-cp27m-win32.whl", hash = "sha256:b6306403228edde6e289f626a3908a2f7f67c344e712cf7c0a508bab3ad9e381"}, + {file = "pycryptodomex-3.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:48697790203909fab02a33226fda546604f4e2653f9d47bc5d3eb40879fa7c64"}, + {file = "pycryptodomex-3.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:18e2ab4813883ae63396c0ffe50b13554b32bb69ec56f0afaf052e7a7ae0d55b"}, + {file = "pycryptodomex-3.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3709f13ca3852b0b07fc04a2c03b379189232b24007c466be0f605dd4723e9d4"}, + {file = "pycryptodomex-3.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:191e73bc84a8064ad1874dba0ebadedd7cce4dedee998549518f2c74a003b2e1"}, + {file = "pycryptodomex-3.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:e3164a18348bd53c69b4435ebfb4ac8a4076291ffa2a70b54f0c4b80c7834b1d"}, + {file = "pycryptodomex-3.15.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:5676a132169a1c1a3712edf25250722ebc8c9102aa9abd814df063ca8362454f"}, + {file = "pycryptodomex-3.15.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e2b12968522a0358b8917fc7b28865acac002f02f4c4c6020fcb264d76bfd06d"}, + {file = "pycryptodomex-3.15.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:e47bf8776a7e15576887f04314f5228c6527b99946e6638cf2f16da56d260cab"}, + {file = "pycryptodomex-3.15.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:996e1ba717077ce1e6d4849af7a1426f38b07b3d173b879e27d5e26d2e958beb"}, + {file = "pycryptodomex-3.15.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:65204412d0c6a8e3c41e21e93a5e6054a74fea501afa03046a388cf042e3377a"}, + {file = "pycryptodomex-3.15.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:dd452a5af7014e866206d41751886c9b4bf379a339fdf2dbfc7dd16c0fb4f8e0"}, + {file = "pycryptodomex-3.15.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:b9279adc16e4b0f590ceff581f53a80179b02cba9056010d733eb4196134a870"}, + {file = "pycryptodomex-3.15.0-cp35-abi3-win32.whl", hash = "sha256:46b3f05f2f7ac7841053da4e0f69616929ca3c42f238c405f6c3df7759ad2780"}, + {file = "pycryptodomex-3.15.0-cp35-abi3-win_amd64.whl", hash = "sha256:8eecdf9cdc7343001d047f951b9cc805cd68cb6cd77b20ea46af5bffc5bd3dfb"}, + {file = "pycryptodomex-3.15.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:67e1e6a92151023ccdfcfbc0afb3314ad30080793b4c27956ea06ab1fb9bcd8a"}, + {file = "pycryptodomex-3.15.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:c4cb9cb492ea7dcdf222a8d19a1d09002798ea516aeae8877245206d27326d86"}, + {file = "pycryptodomex-3.15.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:94c7b60e1f52e1a87715571327baea0733708ab4723346598beca4a3b6879794"}, + {file = "pycryptodomex-3.15.0-pp27-pypy_73-win32.whl", hash = "sha256:04cc393045a8f19dd110c975e30f38ed7ab3faf21ede415ea67afebd95a22380"}, + {file = "pycryptodomex-3.15.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0776bfaf2c48154ab54ea45392847c1283d2fcf64e232e85565f858baedfc1fa"}, + {file = "pycryptodomex-3.15.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:463119d7d22d0fc04a0f9122e9d3e6121c6648bcb12a052b51bd1eed1b996aa2"}, + {file = "pycryptodomex-3.15.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a07a64709e366c2041cd5cfbca592b43998bf4df88f7b0ca73dca37071ccf1bd"}, + {file = "pycryptodomex-3.15.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:35a8f7afe1867118330e2e0e0bf759c409e28557fb1fc2fbb1c6c937297dbe9a"}, + {file = "pycryptodomex-3.15.0.tar.gz", hash = "sha256:7341f1bb2dadb0d1a0047f34c3a58208a92423cdbd3244d998e4b28df5eac0ed"}, ] pygments = [ {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"},