diff --git a/core-spec/expression_language.md b/core-spec/expression_language.md index 42299977..9a4ed2f4 100644 --- a/core-spec/expression_language.md +++ b/core-spec/expression_language.md @@ -139,12 +139,13 @@ Ossie expressions support the following SQL constructs within any expression: Standard SQL operator precedence applies (highest to lowest): 1. Parentheses `()` -2. Unary operators: `+`, `-`, `NOT` +2. Unary operators: `+`, `-` 3. Multiplication/Division: `*`, `/`, `%` -4. Addition/Subtraction: `+`, `-` +4. Addition/Subtraction/Concatenation: `+`, `-`, `||` 5. Comparison: `=`, `<>`, `<`, `>`, `<=`, `>=`, `LIKE`, `IN`, `BETWEEN`, `IS NULL` -6`AND` -7`OR` +6. `NOT` +7. `AND` +8. `OR` --- diff --git a/core/python/.pre-commit-config.yaml b/core/python/.pre-commit-config.yaml new file mode 100644 index 00000000..ea28f357 --- /dev/null +++ b/core/python/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +repos: + - repo: local + hooks: + - id: ossie-sql-ruff-check + name: ossie-sql ruff check + entry: bash -c 'cd core/python && uv run ruff check --fix src tests' + language: system + files: ^core/python/.*\.py$ + pass_filenames: false + - id: ossie-sql-ruff-format + name: ossie-sql ruff format + entry: bash -c 'cd core/python && uv run ruff format src tests' + language: system + files: ^core/python/.*\.py$ + pass_filenames: false + - id: ossie-sql-mypy + name: ossie-sql mypy (strict) + entry: bash -c 'cd core/python && uv run mypy --config-file ../../mypy.ini src tests' + language: system + files: ^core/python/.*\.py$ + pass_filenames: false diff --git a/core/python/Makefile b/core/python/Makefile new file mode 100644 index 00000000..3785b5d4 --- /dev/null +++ b/core/python/Makefile @@ -0,0 +1,13 @@ +.PHONY: format lint typecheck test + +format: + uv run ruff format src tests + +lint: + uv run ruff check src tests + +typecheck: + uv run mypy --config-file ../../mypy.ini src tests + +test: + uv run pytest diff --git a/core/python/README.md b/core/python/README.md new file mode 100644 index 00000000..f64c49ca --- /dev/null +++ b/core/python/README.md @@ -0,0 +1,57 @@ +# apache-ossie-sql + +A [SQLGlot](https://github.com/tobymao/sqlglot) dialect implementing the +Ossie expression language defined in +[`core-spec/expression_language.md`](../../core-spec/expression_language.md) +("Ossie_SQL_2026"). + +This package covers only the expression grammar: a custom SQLGlot `Dialect` +(tokenizer/parser/generator) so `sqlglot.parse_one(sql, read="ossie")` parses +and round-trips the spec's SQL subset (aggregate/window/date/string/math/ +conditional functions, typed literals, `CASE`, `CAST`/`TRY_CAST`, etc.), plus +a validator that rejects the constructs the spec explicitly disallows +(`SELECT`/`FROM`/`JOIN`, `GROUP BY`, `WHERE`, subqueries, CTEs, set +operations, DDL/DML). Wiring the dialect into the Ossie YAML model (the +spec's "Changes to YAML" section) is out of scope here. + +## Development + +This package uses [`uv`](https://docs.astral.sh/uv/) for dependency +management. + +```bash +uv sync + +# Run the test suite +uv run pytest + +# Format code (auto-fixes in place) +uv run ruff format src tests + +# Check formatting without modifying files +uv run ruff format --check src tests + +# Lint +uv run ruff check src tests + +# Type-check (strict; rules come from the shared repo-root mypy.ini) +uv run mypy --config-file ../../mypy.ini src tests +``` + +Or via the `Makefile`: + +```bash +make format +make lint +make typecheck +make test +``` + +### Enforcing formatting/lint/type-checking locally + +Install the pre-commit hooks scoped to this package so `ruff format`, +`ruff check`, and `mypy` run automatically before each commit: + +```bash +uv run pre-commit install -c core/python/.pre-commit-config.yaml +``` diff --git a/core/python/pyproject.toml b/core/python/pyproject.toml new file mode 100644 index 00000000..65513299 --- /dev/null +++ b/core/python/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "apache-ossie-sql" +version = "0.1.0" +description = "SQLGlot dialect for the Ossie expression language (core-spec/expression_language.md)" +license = { text = "Apache-2.0" } +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "sqlglot>=25.0", +] + +[dependency-groups] +dev = [ + "pytest>=8.0", + "mypy>=1.10", + "ruff>=0.11", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/ossie_sql"] + +[tool.ruff] +extend = "../../ruff.toml" + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] diff --git a/core/python/src/ossie_sql/__init__.py b/core/python/src/ossie_sql/__init__.py new file mode 100644 index 00000000..bb13f706 --- /dev/null +++ b/core/python/src/ossie_sql/__init__.py @@ -0,0 +1,44 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""SQLGlot dialect and validation helpers for the Ossie expression language. + +See ``core-spec/expression_language.md`` for the language spec this package +implements, and this package's README for scope and usage. +""" + +from ossie_sql.dialect import Ossie +from ossie_sql.functions import ComplianceLevel, compliance_level +from ossie_sql.identifiers import ( + InvalidIdentifierError, + identifiers_equal, + normalize_identifier, + normalize_identifier_text, +) +from ossie_sql.validate import UnsupportedConstructError, validate_expression + +__all__ = [ + "ComplianceLevel", + "InvalidIdentifierError", + "Ossie", + "UnsupportedConstructError", + "compliance_level", + "identifiers_equal", + "normalize_identifier", + "normalize_identifier_text", + "validate_expression", +] diff --git a/core/python/src/ossie_sql/dialect.py b/core/python/src/ossie_sql/dialect.py new file mode 100644 index 00000000..b659d3da --- /dev/null +++ b/core/python/src/ossie_sql/dialect.py @@ -0,0 +1,205 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""SQLGlot dialect for the Ossie expression language. + +Implements the grammar defined in ``core-spec/expression_language.md`` +("Ossie_SQL_2026"). Registers with SQLGlot as ``"ossie"``, so +``sqlglot.parse_one(sql, read="ossie")`` parses the spec's SQL subset and +``expression.sql(dialect="ossie")`` renders it back. + +The spec is explicitly an ANSI SQL:2003 subset, so this dialect starts from +SQLGlot's default (ANSI-like) Tokenizer/Parser/Generator and only overrides +the handful of spec constructs that the default dialect either can't parse +in the spec's exact shape (``DATEADD``/``DATEDIFF``/``DATE_PART`` with a +bare, leading date-part argument) or renders back in a spelling the spec +doesn't define (e.g. ``STR_POSITION`` instead of ``POSITION ... IN``, +``APPROX_DISTINCT`` instead of ``APPROX_COUNT_DISTINCT``). Functions the spec +lists that SQLGlot has no dedicated AST node for (``IFF``, ``ZEROIFNULL``, +``NULLIFZERO``, ...) already round-trip correctly as-is via SQLGlot's generic +``exp.Anonymous`` fallback and need no customization here. +""" + +from __future__ import annotations + +from collections.abc import Callable, Sequence +from typing import ClassVar + +from sqlglot import TokenType, exp +from sqlglot.dialects.dialect import Dialect, unit_to_var +from sqlglot.generator import Generator +from sqlglot.helper import seq_get +from sqlglot.parser import Parser +from sqlglot.tokens import Tokenizer + +_FuncBuilder = Callable[[Sequence[exp.Expression]], exp.Expression] + + +def _build_date_delta(exp_class: type[exp.DateAdd] | type[exp.DateDiff]) -> _FuncBuilder: + """Build a parser for ``FUNC(part, amount_or_start, date_or_end)``. + + The spec puts the date-part identifier FIRST (``DATEADD(day, 7, d)``, + ``DATEDIFF(day, d1, d2)``). SQLGlot's generic positional ``Func`` + construction instead treats the *last* argument as the unit, so this + needs a dedicated builder rather than relying on the default mapping. + """ + + def _builder(args: Sequence[exp.Expression]) -> exp.Expression: + return exp_class(this=seq_get(args, 2), expression=seq_get(args, 1), unit=seq_get(args, 0)) + + return _builder + + +def _build_date_part(args: Sequence[exp.Expression]) -> exp.Expression: + """``DATE_PART(part, expr)`` is the spec's alternative spelling of + ``EXTRACT(part FROM expr)``; unify both into the same ``exp.Extract`` + node so callers see one canonical AST shape regardless of which + surface syntax was used. + + ``DATE_PART`` takes its part name as a quoted string (``'year'``) while + ``EXTRACT`` takes a bare keyword (``YEAR``); normalize to the latter so + the merged AST always renders as valid ``EXTRACT(... FROM ...)`` syntax. + """ + part = seq_get(args, 0) + part_name = part.name if part is not None else "" + return exp.Extract(this=exp.var(part_name.upper()), expression=seq_get(args, 1)) + + +def _build_dpipe( + *, this: exp.Expression | None = None, expression: exp.Expression | None = None +) -> exp.DPipe: + """Give ``||`` the same precedence tier as binary ``+``/``-``. + + SQLGlot's default parser groups ``||`` with the bitwise operators, one + tier looser than ``+``/``-``. The spec instead follows the common convention, + which both put ``||`` at the *same* tier as ``+``/``-`` + (left-to-right, like the arithmetic operators around it) -- see + ``_OssieParser.TERM`` below, which is what actually makes that happen; + this just reproduces the ``safe=True`` default SQLGlot's own DPipe + construction uses so behavior is otherwise unchanged. + """ + return exp.DPipe(this=this, expression=expression, safe=True) + + +class _OssieTokenizer(Tokenizer): + pass + + +class _OssieParser(Parser): + FUNCTIONS = { + **Parser.FUNCTIONS, + "DATEADD": _build_date_delta(exp.DateAdd), + "DATEDIFF": _build_date_delta(exp.DateDiff), + "DATE_PART": _build_date_part, + "APPROX_PERCENTILE": exp.ApproxQuantile.from_arg_list, + } + # SQLGlot infers Parser.TERM's type from its literal (dict[TokenType, + # type[Binary]]), so mypy sees adding a plain builder function as an + # incompatible override -- it isn't, at runtime _parse_term only ever + # calls `klass(this=..., expression=...)` generically. Silence the two + # resulting checks rather than fight SQLGlot's own inferred type. + TERM: ClassVar[dict[TokenType, Callable[..., exp.Expression]]] = { # type: ignore[assignment] + **Parser.TERM, # type: ignore[dict-item] + TokenType.DPIPE: _build_dpipe, + } + + +class _OssieGenerator(Generator): + TYPE_MAPPING = { + **Generator.TYPE_MAPPING, + exp.DataType.Type.TIMESTAMPNTZ: "TIMESTAMP_NTZ", + } + + def dateadd_sql(self, expression: exp.DateAdd) -> str: + return self.func("DATEADD", unit_to_var(expression), expression.expression, expression.this) + + def datediff_sql(self, expression: exp.DateDiff) -> str: + return self.func( + "DATEDIFF", unit_to_var(expression), expression.expression, expression.this + ) + + def strposition_sql(self, expression: exp.StrPosition) -> str: + this = self.sql(expression, "this") + substr = self.sql(expression, "substr") + return f"POSITION({substr} IN {this})" + + def startswith_sql(self, expression: exp.StartsWith) -> str: + return self.func("STARTSWITH", expression.this, expression.expression) + + def endswith_sql(self, expression: exp.EndsWith) -> str: + return self.func("ENDSWITH", expression.this, expression.expression) + + def approxdistinct_sql(self, expression: exp.ApproxDistinct) -> str: + return self.func("APPROX_COUNT_DISTINCT", expression.this, expression.args.get("accuracy")) + + def variancepop_sql(self, expression: exp.VariancePop) -> str: + # SQLGlot's own canonical spelling ("VARIANCE_POP") isn't a name the + # spec recognizes at all; the spec only defines "VAR_POP". + return self.func("VAR_POP", expression.this) + + def dayofyear_sql(self, expression: exp.DayOfYear) -> str: + # Ditto: the spec defines "DAYOFYEAR", not SQLGlot's "DAY_OF_YEAR". + return self.func("DAYOFYEAR", expression.this) + + def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str: + # The spec calls this "APPROX_PERCENTILE"; SQLGlot's canonical name + # ("APPROX_QUANTILE") isn't a spelling the spec defines. + return self.func("APPROX_PERCENTILE", expression.this, expression.args.get("quantile")) + + def not_sql(self, expression: exp.Not) -> str: + # Render the compact `NOT IN`/`IS NOT` forms the spec documents, + # instead of SQLGlot's generic `NOT (x IN (...))` / `NOT (x IS NULL)`. + this = expression.this + if isinstance(this, exp.In): + return self.in_sql(this).replace(" IN ", " NOT IN ", 1) + if isinstance(this, exp.Is): + return self.binary(this, "IS NOT") + return super().not_sql(expression) + + def tochar_sql(self, expression: exp.ToChar) -> str: + return self.func("TO_CHAR", expression.this, expression.args.get("format")) + + def cast_sql(self, expression: exp.Cast, safe_prefix: str | None = None) -> str: + # Prefer the compact typed-literal form (`DATE '...'`) for a string + # literal cast to DATE/TIME/TIMESTAMP/TIMESTAMP_NTZ -- the spec's + # primary documented construction syntax for these types. + to = expression.to + this = expression.this + if ( + not safe_prefix + and isinstance(this, exp.Literal) + and this.is_string + and to.is_type( + exp.DataType.Type.DATE, + exp.DataType.Type.TIME, + exp.DataType.Type.TIMESTAMP, + exp.DataType.Type.TIMESTAMPNTZ, + ) + ): + return f"{self.sql(to)} {self.sql(this)}" + return super().cast_sql(expression, safe_prefix=safe_prefix) + + +class Ossie(Dialect): + """The Ossie_SQL_2026 expression dialect.""" + + Tokenizer = _OssieTokenizer + Parser = _OssieParser + Generator = _OssieGenerator + + +__all__ = ["Ossie"] diff --git a/core/python/src/ossie_sql/functions.py b/core/python/src/ossie_sql/functions.py new file mode 100644 index 00000000..931b829d --- /dev/null +++ b/core/python/src/ossie_sql/functions.py @@ -0,0 +1,254 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Function-name compliance levels from ``core-spec/expression_language.md``. + +This module is purely informational (see the package README / project plan +for the rationale): the spec's own "Dialect Extensions" section says an +unrecognized function name should pass through by default, so +:mod:`ossie_sql.validate` does *not* use this module to reject expressions. +Instead, :func:`compliance_level` lets callers (tests, tooling, documentation +generators, ...) ask "is this function name part of the portable core the +spec defines, and at what tier?" + +This may be used in the future for strict modes and generating warnings. + +* Only function *names* are listed here -- operators (``||``, ``%``, + ``AND``/``OR``/``NOT``, ``LIKE``/``ILIKE``) and typed literals + (``DATE '...'``) are syntax, not callable names, and don't belong in a + function-name registry. +* The spec's date-part vocabulary for ``EXTRACT``/``DATE_PART`` (``WEEK``, + ``DAYOFWEEK``, ``MILLISECOND``, ...) is a separate, closed list of + *argument* values, not standalone function names, and is intentionally + excluded from :data:`DATETIME_FUNCTIONS`. +* Names like ``TO_VARCHAR``/``TO_NUMBER``/``TO_BOOLEAN`` appear in some + draft implementations but are not defined anywhere in the committed + spec, so they are not included here. +""" + +from __future__ import annotations + +from typing import Final, Literal + +ComplianceLevel = Literal["REQUIRED", "RECOMMENDED", "EXPERIMENTAL"] + +# Aggregation -- Core (REQUIRED), Statistical (REQUIRED), Percentile (REQUIRED), +# Approximate (RECOMMENDED). +_AGGREGATE_REQUIRED: Final[frozenset[str]] = frozenset( + { + "SUM", + "COUNT", + "AVG", + "MIN", + "MAX", + "STDDEV", + "STDDEV_POP", + "STDDEV_SAMP", + "VARIANCE", + "VAR_POP", + "VAR_SAMP", + "MEDIAN", + "PERCENTILE_CONT", + "PERCENTILE_DISC", + } +) +_AGGREGATE_RECOMMENDED: Final[frozenset[str]] = frozenset( + { + "APPROX_COUNT_DISTINCT", + "APPROX_PERCENTILE", + } +) + +# Date/Time -- Current, Extraction, Alternative Extraction Syntax, Truncation, +# Arithmetic, and Construction are all REQUIRED. Formatting (TO_CHAR) and +# format-string construction are EXPERIMENTAL per the spec's own section +# headers. +_DATETIME_REQUIRED: Final[frozenset[str]] = frozenset( + { + "CURRENT_DATE", + "CURRENT_TIMESTAMP", + "CURRENT_TIME", + "YEAR", + "QUARTER", + "MONTH", + "DAY", + "DAYOFYEAR", + "HOUR", + "MINUTE", + "SECOND", + "EXTRACT", + "DATE_PART", + "DATE_TRUNC", + "DATEADD", + "DATEDIFF", + "TO_DATE", + "TO_TIMESTAMP", + } +) +_DATETIME_EXPERIMENTAL: Final[frozenset[str]] = frozenset({"TO_CHAR"}) + +# String -- Manipulation and Search are REQUIRED (including REGEXP_LIKE, listed +# under Pattern Matching); the RECOMMENDED regex functions are separate. +_STRING_REQUIRED: Final[frozenset[str]] = frozenset( + { + "CONCAT", + "LENGTH", + "LOWER", + "UPPER", + "TRIM", + "LTRIM", + "RTRIM", + "LEFT", + "RIGHT", + "SUBSTRING", + "REPLACE", + "SPLIT_PART", + "POSITION", + "CHARINDEX", + "CONTAINS", + "STARTSWITH", + "ENDSWITH", + "REGEXP_LIKE", + } +) +_STRING_RECOMMENDED: Final[frozenset[str]] = frozenset( + { + "REGEXP_EXTRACT", + "REGEXP_REPLACE", + "REGEXP_COUNT", + } +) + +# Math -- Basic and Advanced are REQUIRED; Trigonometric is RECOMMENDED. +_MATH_REQUIRED: Final[frozenset[str]] = frozenset( + { + "ABS", + "ROUND", + "FLOOR", + "CEIL", + "CEILING", + "TRUNC", + "TRUNCATE", + "MOD", + "SIGN", + "POWER", + "SQRT", + "EXP", + "LN", + "LOG", + "LOG10", + "GREATEST", + "LEAST", + } +) +_MATH_RECOMMENDED: Final[frozenset[str]] = frozenset( + { + "SIN", + "COS", + "TAN", + "ASIN", + "ACOS", + "ATAN", + "ATAN2", + "RADIANS", + "DEGREES", + "PI", + } +) + +# Conditional (REQUIRED). +_CONDITIONAL_REQUIRED: Final[frozenset[str]] = frozenset( + { + "IF", + "IFF", + "NULLIF", + "COALESCE", + "IFNULL", + "NVL", + "NVL2", + "ZEROIFNULL", + "NULLIFZERO", + } +) + +# Window -- Ranking and Offset (REQUIRED). Window aggregations reuse the +# aggregate functions above. +_WINDOW_REQUIRED: Final[frozenset[str]] = frozenset( + { + "ROW_NUMBER", + "RANK", + "DENSE_RANK", + "NTILE", + "PERCENT_RANK", + "CUME_DIST", + "LAG", + "LEAD", + "FIRST_VALUE", + "LAST_VALUE", + "NTH_VALUE", + } +) + +# Type conversion -- CAST is REQUIRED, TRY_CAST is RECOMMENDED. +_TYPE_CONVERSION_REQUIRED: Final[frozenset[str]] = frozenset({"CAST"}) +_TYPE_CONVERSION_RECOMMENDED: Final[frozenset[str]] = frozenset({"TRY_CAST"}) + +REQUIRED_FUNCTIONS: Final[frozenset[str]] = ( + _AGGREGATE_REQUIRED + | _DATETIME_REQUIRED + | _STRING_REQUIRED + | _MATH_REQUIRED + | _CONDITIONAL_REQUIRED + | _WINDOW_REQUIRED + | _TYPE_CONVERSION_REQUIRED +) +"""Every function name the spec marks REQUIRED (MUST support).""" + +RECOMMENDED_FUNCTIONS: Final[frozenset[str]] = ( + _AGGREGATE_RECOMMENDED | _STRING_RECOMMENDED | _MATH_RECOMMENDED | _TYPE_CONVERSION_RECOMMENDED +) +"""Every function name the spec marks RECOMMENDED (SHOULD support).""" + +EXPERIMENTAL_FUNCTIONS: Final[frozenset[str]] = _DATETIME_EXPERIMENTAL +"""Function names the spec marks EXPERIMENTAL (format-string-driven date/time).""" + + +def compliance_level(name: str) -> ComplianceLevel | None: + """Return the spec compliance tier for a function name, or ``None``. + + ``None`` means ``name`` is not defined anywhere in + ``core-spec/expression_language.md`` -- per the spec's "Dialect + Extensions" section, that makes it a vendor/dialect extension, which + :mod:`ossie_sql.validate` passes through rather than rejects. + """ + upper = name.upper() + if upper in REQUIRED_FUNCTIONS: + return "REQUIRED" + if upper in RECOMMENDED_FUNCTIONS: + return "RECOMMENDED" + if upper in EXPERIMENTAL_FUNCTIONS: + return "EXPERIMENTAL" + return None + + +__all__ = [ + "REQUIRED_FUNCTIONS", + "RECOMMENDED_FUNCTIONS", + "EXPERIMENTAL_FUNCTIONS", + "ComplianceLevel", + "compliance_level", +] diff --git a/core/python/src/ossie_sql/identifiers.py b/core/python/src/ossie_sql/identifiers.py new file mode 100644 index 00000000..9768d021 --- /dev/null +++ b/core/python/src/ossie_sql/identifiers.py @@ -0,0 +1,103 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Identifier normalization per the spec's "Namespacing and Identifier +Resolution" section. + +Key rule from the spec's comparison table: **regular (unquoted) identifiers +normalize to upper-case**, while quoted identifiers are matched exactly +(quotes stripped, escapes unescaped, case preserved) -- + + id -> ID (regular identifier, case-insensitive, normalizes upper) + Id -> ID (same) + "ID" -> ID (quoted, but happens to already be upper) + "id" -> id (quoted: exact, case preserved -- does NOT match column ID) + +Identifiers must be valid ANSI SQL names up to 128 characters. +""" + +from __future__ import annotations + +import re +from typing import Final + +from sqlglot import exp + +_MAX_IDENTIFIER_LENGTH: Final[int] = 128 + +# ANSI SQL regular (unquoted) identifier shape: a letter, followed by any run +# of letters/digits/underscores. The spec doesn't spell out a formal grammar +# beyond "follow ANSI SQL naming"; this matches the common, portable subset +# every major engine accepts unquoted. +_REGULAR_IDENTIFIER_RE = re.compile(r"\A[A-Za-z][A-Za-z0-9_]*\Z") + + +class InvalidIdentifierError(ValueError): + """Raised when an identifier doesn't meet the spec's shape/length rules.""" + + +def is_valid_identifier(raw: str, *, quoted: bool) -> bool: + """Return whether ``raw`` is a syntactically valid Ossie identifier. + + ``raw`` is the identifier text with quotes already stripped (as + SQLGlot's ``exp.Identifier.this`` provides). Quoted identifiers accept + any non-empty text; regular (unquoted) identifiers must match ANSI + regular-identifier shape. Both are capped at 128 characters. + """ + if not raw or len(raw) > _MAX_IDENTIFIER_LENGTH: + return False + if quoted: + return True + return bool(_REGULAR_IDENTIFIER_RE.match(raw)) + + +def normalize_identifier_text(raw: str, *, quoted: bool) -> str: + """Return the spec-normalized form of identifier text. + + Regular identifiers case-fold to upper-case (matching the spec's + normalization rule); quoted identifiers are returned unchanged. + + Raises + ------ + InvalidIdentifierError + If ``raw`` doesn't meet the shape/length rules. + """ + if not is_valid_identifier(raw, quoted=quoted): + raise InvalidIdentifierError( + f"{raw!r} is not a valid Ossie identifier " + f"(quoted={quoted}, max length {_MAX_IDENTIFIER_LENGTH})" + ) + return raw if quoted else raw.upper() + + +def normalize_identifier(node: exp.Identifier) -> str: + """Return the spec-normalized form of a SQLGlot ``exp.Identifier`` node.""" + return normalize_identifier_text(node.this, quoted=bool(node.args.get("quoted"))) + + +def identifiers_equal(a: exp.Identifier, b: exp.Identifier) -> bool: + """Return whether two identifiers refer to the same normalized name.""" + return normalize_identifier(a) == normalize_identifier(b) + + +__all__ = [ + "InvalidIdentifierError", + "is_valid_identifier", + "normalize_identifier_text", + "normalize_identifier", + "identifiers_equal", +] diff --git a/core/python/src/ossie_sql/py.typed b/core/python/src/ossie_sql/py.typed new file mode 100644 index 00000000..13a83393 --- /dev/null +++ b/core/python/src/ossie_sql/py.typed @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/core/python/src/ossie_sql/validate.py b/core/python/src/ossie_sql/validate.py new file mode 100644 index 00000000..e543dc36 --- /dev/null +++ b/core/python/src/ossie_sql/validate.py @@ -0,0 +1,91 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Construct validation per the spec's "Not Supported in Expressions" table. + +Ossie expressions are scalar/aggregate/window fragments, not full queries. +The spec explicitly disallows ``SELECT``/``FROM``/``JOIN``, ``GROUP BY``, +``WHERE``, subqueries, CTEs, set operations (``UNION``/``INTERSECT``/ +``EXCEPT``), and DDL/DML -- each with a documented reason ("use filter +property instead", "use field references instead", etc.). + +Per the spec's "Dialect Extensions" section, unrecognized *function names* +pass through by default (see :mod:`ossie_sql.functions`); this module only +rejects the constructs the spec explicitly disallows. It does not maintain +a function-name whitelist. + +Bind parameters/placeholders (``:n``, ``?``) are not documented anywhere in +the expression language -- Ossie expressions are static field/metric +bodies, not parameterized queries -- so they're rejected here too. +""" + +from __future__ import annotations + +from sqlglot import exp + +from ossie_sql.windows import first_unsupported_frame + +# WHERE/GROUP BY/JOIN clauses can only appear inside a SELECT statement in +# SQL grammar, so rejecting exp.Select transitively covers them; exp.Join is +# listed explicitly too since the spec calls it out by name in its own row. +_DISALLOWED_NODE_TYPES: tuple[type[exp.Expression], ...] = ( + exp.Select, + exp.With, + exp.Union, + exp.Intersect, + exp.Except, + exp.Join, + exp.Create, + exp.Drop, + exp.Alter, + exp.Insert, + exp.Update, + exp.Delete, + exp.Merge, + exp.Placeholder, + exp.Parameter, +) + + +class UnsupportedConstructError(ValueError): + """Raised when an expression contains a construct the spec disallows.""" + + def __init__(self, node: exp.Expression) -> None: + self.node = node + super().__init__( + f"{type(node).__name__!r} is not a supported Ossie expression construct: {node.sql()!r}" + ) + + +def validate_expression(expression: exp.Expr) -> None: + """Raise :class:`UnsupportedConstructError` for any disallowed construct. + + Walks the full AST (not just the top level) so a disallowed construct + nested inside an otherwise-valid expression (e.g. a subquery inside an + ``IN`` list) is still caught. Does not check function names -- see the + module docstring. + """ + for node in expression.walk(): + if isinstance(node, _DISALLOWED_NODE_TYPES): + raise UnsupportedConstructError(node) + + unsupported_frame = first_unsupported_frame(expression) + if unsupported_frame is not None: + raise UnsupportedConstructError(unsupported_frame) + + +__all__ = ["UnsupportedConstructError", "validate_expression"] diff --git a/core/python/src/ossie_sql/windows.py b/core/python/src/ossie_sql/windows.py new file mode 100644 index 00000000..ccd1e45b --- /dev/null +++ b/core/python/src/ossie_sql/windows.py @@ -0,0 +1,64 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Window-function helpers matching the spec's "Window Functions" section. + +The spec documents exactly two frame modes -- ``ROWS`` and ``RANGE`` -- and +says window functions "should act consistently with window functions in +ANSI SQL", whose core (SQL:2003, the standard this whole language is based +on per the spec's "Standards Reference" section) only defines those two +frame modes; ``GROUPS`` is a later (SQL:2011) addition. So ``GROUPS`` frames +are treated as out of scope here. + +Note: an earlier draft implementation (PR #125) also rejected *nested* +window functions and *parameterized* frame bounds. Neither restriction +appears anywhere in the committed spec text, so this module does not +enforce them -- per this package's policy of treating the committed spec as +authoritative over earlier drafts. +""" + +from __future__ import annotations + +from sqlglot import exp + +_ACCEPTED_FRAME_KINDS = frozenset({"ROWS", "RANGE"}) + + +def contains_window(expression: exp.Expr) -> bool: + """Return whether ``expression``'s AST contains an ``OVER (...)`` window.""" + return any(isinstance(node, exp.Window) for node in expression.walk()) + + +def first_unsupported_frame(expression: exp.Expr) -> exp.Window | None: + """Return the first window whose frame clause uses an unsupported mode. + + Returns ``None`` if every ``OVER (...)`` in ``expression`` either omits a + frame clause or uses ``ROWS``/``RANGE``. + """ + for node in expression.walk(): + if not isinstance(node, exp.Window): + continue + spec = node.args.get("spec") + if spec is None: + continue + kind = (spec.args.get("kind") or "").upper() + if kind and kind not in _ACCEPTED_FRAME_KINDS: + return node + return None + + +__all__ = ["contains_window", "first_unsupported_frame"] diff --git a/core/python/tests/conftest.py b/core/python/tests/conftest.py new file mode 100644 index 00000000..4857ae18 --- /dev/null +++ b/core/python/tests/conftest.py @@ -0,0 +1,45 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Shared test fixtures. + +Importing ``ossie_sql`` registers the ``"ossie"`` dialect with SQLGlot (via +the ``Ossie(Dialect)`` subclass's registration metaclass), so every test +module can call ``sqlglot.parse_one(sql, read="ossie")`` / +``expression.sql(dialect="ossie")`` without an explicit import of +``ossie_sql.dialect``. +""" + +from __future__ import annotations + +from collections.abc import Callable + +import pytest +import sqlglot +from sqlglot import exp + +import ossie_sql # noqa: F401 (registers the "ossie" dialect as a side effect) + + +@pytest.fixture +def parse_ossie() -> Callable[[str], exp.Expr]: + """Return a helper that parses SQL text with the Ossie dialect.""" + + def _parse(sql: str) -> exp.Expr: + return sqlglot.parse_one(sql, read="ossie") + + return _parse diff --git a/core/python/tests/test_dialect_roundtrip.py b/core/python/tests/test_dialect_roundtrip.py new file mode 100644 index 00000000..f70ea196 --- /dev/null +++ b/core/python/tests/test_dialect_roundtrip.py @@ -0,0 +1,144 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Parse -> generate round-trip tests across the spec's construct categories. + +Each case is (input, expected_output) using the Ossie dialect for both +parsing and rendering. Most cases round-trip to themselves; a few +canonicalize to a different (still spec-valid) spelling, which is called +out inline. +""" + +from __future__ import annotations + +import pytest +import sqlglot + +import ossie_sql # noqa: F401 + +ROUNDTRIP_CASES = [ + # Arithmetic / comparison / logical operators. + ("a + b - c * d / e % f", "a + b - c * d / e % f"), + ("a = b AND a <> b AND a != b", "a = b AND a <> b AND a <> b"), + ("a < b OR NOT (a > b)", "a < b OR NOT (a > b)"), + ("x BETWEEN a AND b", "x BETWEEN a AND b"), + ("x IN (a, b, c)", "x IN (a, b, c)"), + ("x NOT IN (a, b, c)", "x NOT IN (a, b, c)"), + ("x LIKE 'a%'", "x LIKE 'a%'"), + ("x ILIKE 'a%'", "x ILIKE 'a%'"), + ("x IS NULL", "x IS NULL"), + ("x IS NOT NULL", "x IS NOT NULL"), + ("NOT x IS NULL", "x IS NOT NULL"), + ("a IS DISTINCT FROM b", "a IS DISTINCT FROM b"), + ("a IS NOT DISTINCT FROM b", "a IS NOT DISTINCT FROM b"), + ("a || b", "a || b"), + # || sits at the same precedence tier as binary +/- (Snowflake/Databricks + # convention, not SQLGlot's default looser bitwise-adjacent tier). + ("a + b || c", "a + b || c"), + ("a || b + c", "a || b + c"), + ("a || b * c", "a || b * c"), + # CASE. + ("CASE WHEN a THEN 1 WHEN b THEN 2 ELSE 3 END", "CASE WHEN a THEN 1 WHEN b THEN 2 ELSE 3 END"), + ("CASE x WHEN 1 THEN 'a' ELSE 'b' END", "CASE x WHEN 1 THEN 'a' ELSE 'b' END"), + # Aggregates. + ("SUM(amount)", "SUM(amount)"), + ("COUNT(*)", "COUNT(*)"), + ("COUNT(DISTINCT customer_id)", "COUNT(DISTINCT customer_id)"), + ("SUM(DISTINCT amount)", "SUM(DISTINCT amount)"), + ("MEDIAN(x)", "MEDIAN(x)"), + ( + "PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY x)", + "PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY x)", + ), + ("APPROX_COUNT_DISTINCT(customer_id)", "APPROX_COUNT_DISTINCT(customer_id)"), + ("APPROX_PERCENTILE(amount, 0.5)", "APPROX_PERCENTILE(amount, 0.5)"), + ("VAR_POP(x)", "VAR_POP(x)"), + ("VAR_SAMP(x)", "VARIANCE(x)"), # spec declares VAR_SAMP an alias for VARIANCE + # Date/time. + ("YEAR(order_date)", "YEAR(order_date)"), + ("DAYOFYEAR(order_date)", "DAYOFYEAR(order_date)"), + ("EXTRACT(YEAR FROM order_date)", "EXTRACT(YEAR FROM order_date)"), + ("DATE_PART('year', order_date)", "EXTRACT(YEAR FROM order_date)"), # unified onto EXTRACT + # DATE_TRUNC's unit literal is normalized to upper-case at AST + # construction time (a SQLGlot-wide invariant, not dialect-specific) -- + # harmless since the part name is compared case-insensitively everywhere. + ("DATE_TRUNC('month', order_date)", "DATE_TRUNC('MONTH', order_date)"), + ("DATEADD(day, 7, order_date)", "DATEADD(DAY, 7, order_date)"), + ("DATEDIFF(day, start_date, end_date)", "DATEDIFF(DAY, start_date, end_date)"), + ("TO_CHAR(order_date, 'YYYY-MM-DD')", "TO_CHAR(order_date, 'YYYY-MM-DD')"), + # String. + ("CONCAT(a, b)", "CONCAT(a, b)"), + ("POSITION('a' IN b)", "POSITION('a' IN b)"), + ("CHARINDEX('a', b)", "POSITION('a' IN b)"), # alias for POSITION, per spec + ("STARTSWITH(a, 'x')", "STARTSWITH(a, 'x')"), + ("ENDSWITH(a, 'x')", "ENDSWITH(a, 'x')"), + ("CONTAINS(a, 'x')", "CONTAINS(a, 'x')"), + ("REGEXP_LIKE(a, 'x.*')", "REGEXP_LIKE(a, 'x.*')"), + # Math. + ("ABS(x)", "ABS(x)"), + ("CEIL(x)", "CEIL(x)"), + ("CEILING(x)", "CEIL(x)"), # spec-declared alias + ("MOD(x, y)", "x % y"), # spec also defines "%" as the operator form + ("GREATEST(a, b, c)", "GREATEST(a, b, c)"), + # Conditional. + ("IFF(a > b, 1, 0)", "IFF(a > b, 1, 0)"), + ("COALESCE(a, b, c)", "COALESCE(a, b, c)"), + ("NVL2(a, 1, 0)", "NVL2(a, 1, 0)"), + ("ZEROIFNULL(x)", "ZEROIFNULL(x)"), + ("NULLIFZERO(x)", "NULLIFZERO(x)"), + # Window. + ("ROW_NUMBER() OVER (ORDER BY x)", "ROW_NUMBER() OVER (ORDER BY x)"), + ( + "SUM(amount) OVER (PARTITION BY region ORDER BY order_date " + "ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)", + "SUM(amount) OVER (PARTITION BY region ORDER BY order_date " + "ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)", + ), + ("LAG(x, 1, 0) OVER (ORDER BY d)", "LAG(x, 1, 0) OVER (ORDER BY d)"), + # Type conversion / typed literals. + ("CAST(a AS VARCHAR)", "CAST(a AS VARCHAR)"), + ("TRY_CAST(a AS INTEGER)", "TRY_CAST(a AS INT)"), + ("DATE '2024-01-15'", "DATE '2024-01-15'"), + ("TIME '10:30:00'", "TIME '10:30:00'"), + ("TIMESTAMP_NTZ '2024-01-15 10:30:00'", "TIMESTAMP_NTZ '2024-01-15 10:30:00'"), +] + + +@pytest.mark.parametrize("source,expected", ROUNDTRIP_CASES) +def test_roundtrip(source: str, expected: str) -> None: + parsed = sqlglot.parse_one(source, read="ossie") + assert parsed.sql(dialect="ossie") == expected + + +# Precedence-structure checks: `a + b || c` renders identically either way +# `||` groups relative to `+`/`-`, so the string-equality cases above alone +# wouldn't catch a regression back to SQLGlot's default (looser) precedence +# for `||`. Assert the actual top-level node type instead, matching the +# Snowflake/Databricks convention of `||` sharing +/-'s tier, left to right. +CONCAT_PRECEDENCE_CASES = [ + ("a + b || c", sqlglot.exp.DPipe), # (a + b) || c + ("a || b + c", sqlglot.exp.Add), # (a || b) + c + ("a || b * c", sqlglot.exp.DPipe), # a || (b * c) -- * still binds tighter +] + + +@pytest.mark.parametrize("source,top_level_type", CONCAT_PRECEDENCE_CASES) +def test_concat_precedence_matches_term_level( + source: str, top_level_type: type[sqlglot.exp.Expression] +) -> None: + parsed = sqlglot.parse_one(source, read="ossie") + assert type(parsed) is top_level_type diff --git a/core/python/tests/test_functions.py b/core/python/tests/test_functions.py new file mode 100644 index 00000000..18a18aee --- /dev/null +++ b/core/python/tests/test_functions.py @@ -0,0 +1,114 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Tests for ossie_sql.functions.compliance_level().""" + +from __future__ import annotations + +import pytest + +from ossie_sql.functions import ( + EXPERIMENTAL_FUNCTIONS, + RECOMMENDED_FUNCTIONS, + REQUIRED_FUNCTIONS, + compliance_level, +) + +REQUIRED_EXAMPLES = [ + "SUM", + "COUNT", + "AVG", + "MEDIAN", + "PERCENTILE_CONT", + "YEAR", + "DATEADD", + "DATEDIFF", + "EXTRACT", + "DATE_PART", + "CONCAT", + "POSITION", + "STARTSWITH", + "ABS", + "MOD", + "IF", + "IFF", + "ZEROIFNULL", + "ROW_NUMBER", + "LAG", + "CAST", +] + +RECOMMENDED_EXAMPLES = [ + "APPROX_COUNT_DISTINCT", + "APPROX_PERCENTILE", + "REGEXP_EXTRACT", + "SIN", + "PI", + "TRY_CAST", +] + +EXPERIMENTAL_EXAMPLES = ["TO_CHAR"] + + +@pytest.mark.parametrize("name", REQUIRED_EXAMPLES) +def test_required_functions(name: str) -> None: + assert compliance_level(name) == "REQUIRED" + + +@pytest.mark.parametrize("name", RECOMMENDED_EXAMPLES) +def test_recommended_functions(name: str) -> None: + assert compliance_level(name) == "RECOMMENDED" + + +@pytest.mark.parametrize("name", EXPERIMENTAL_EXAMPLES) +def test_experimental_functions(name: str) -> None: + assert compliance_level(name) == "EXPERIMENTAL" + + +@pytest.mark.parametrize("name", ["sum", "Sum", "sUM"]) +def test_compliance_level_is_case_insensitive(name: str) -> None: + assert compliance_level(name) == "REQUIRED" + + +@pytest.mark.parametrize( + "name", + [ + "SNOWFLAKE_VENDOR_FUNC", + "TO_VARCHAR", + "TO_NUMBER", + "TO_BOOLEAN", + "EXISTS_IN", + "NOT_A_FUNCTION", + ], +) +def test_unknown_functions_return_none(name: str) -> None: + # These are either vendor extensions or names from an earlier draft that + # the committed spec never defines -- compliance_level() reports them as + # unknown (None) rather than raising, matching the spec's pass-through + # philosophy for anything outside its own tables. + assert compliance_level(name) is None + + +def test_tiers_are_disjoint() -> None: + assert not (REQUIRED_FUNCTIONS & RECOMMENDED_FUNCTIONS) + assert not (REQUIRED_FUNCTIONS & EXPERIMENTAL_FUNCTIONS) + assert not (RECOMMENDED_FUNCTIONS & EXPERIMENTAL_FUNCTIONS) + + +def test_all_names_are_upper_case() -> None: + for name in REQUIRED_FUNCTIONS | RECOMMENDED_FUNCTIONS | EXPERIMENTAL_FUNCTIONS: + assert name == name.upper() diff --git a/core/python/tests/test_identifiers.py b/core/python/tests/test_identifiers.py new file mode 100644 index 00000000..47fc4f51 --- /dev/null +++ b/core/python/tests/test_identifiers.py @@ -0,0 +1,102 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Tests for ossie_sql.identifiers, matching the spec's comparison table. + +You type this | Equivalent to | Matches a column created as `id`? +id | ID | Yes (standard behavior) +Id | ID | Yes (standard behavior) +"ID" | ID | Yes (force-matched to normalized case) +"id" | id | No (quotes force exact lower-case match) +""" + +from __future__ import annotations + +import pytest +import sqlglot + +import ossie_sql # noqa: F401 +from ossie_sql.identifiers import ( + InvalidIdentifierError, + identifiers_equal, + is_valid_identifier, + normalize_identifier, + normalize_identifier_text, +) + + +def _identifier(sql: str) -> sqlglot.exp.Identifier: + column = sqlglot.parse_one(sql, read="ossie") + assert isinstance(column, sqlglot.exp.Column) + ident = column.this + assert isinstance(ident, sqlglot.exp.Identifier) + return ident + + +@pytest.mark.parametrize( + "source,expected_normalized", + [ + ("id", "ID"), + ("Id", "ID"), + ('"ID"', "ID"), + ('"id"', "id"), + ], +) +def test_normalize_identifier_matches_spec_table(source: str, expected_normalized: str) -> None: + assert normalize_identifier(_identifier(source)) == expected_normalized + + +@pytest.mark.parametrize( + "a,b,expected_equal", + [ + ("id", "ID", True), + ("Id", "ID", True), + ('"ID"', "ID", True), + ('"id"', "ID", False), # quoted lower-case does NOT match column ID + ], +) +def test_identifiers_equal_matches_spec_table(a: str, b: str, expected_equal: bool) -> None: + assert identifiers_equal(_identifier(a), _identifier(b)) is expected_equal + + +def test_regular_identifier_must_start_with_a_letter() -> None: + assert is_valid_identifier("abc123", quoted=False) + assert not is_valid_identifier("123abc", quoted=False) + + +def test_regular_identifier_rejects_special_characters() -> None: + assert not is_valid_identifier("a-b", quoted=False) + assert not is_valid_identifier("a b", quoted=False) + + +def test_quoted_identifier_allows_arbitrary_text() -> None: + assert is_valid_identifier("a-b c!", quoted=True) + + +def test_identifier_length_limit_is_128() -> None: + ok = "a" * 128 + too_long = "a" * 129 + assert is_valid_identifier(ok, quoted=False) + assert not is_valid_identifier(too_long, quoted=False) + assert not is_valid_identifier(too_long, quoted=True) + + +def test_normalize_invalid_identifier_raises() -> None: + with pytest.raises(InvalidIdentifierError): + normalize_identifier_text("123abc", quoted=False) + with pytest.raises(InvalidIdentifierError): + normalize_identifier_text("", quoted=True) diff --git a/core/python/tests/test_typed_literals.py b/core/python/tests/test_typed_literals.py new file mode 100644 index 00000000..8015f33f --- /dev/null +++ b/core/python/tests/test_typed_literals.py @@ -0,0 +1,68 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Typed-literal and CAST/TRY_CAST construction tests. + +See the spec's "Date/Time Construction" and "Type Conversion Functions" +sections: typed literals (``DATE '...'``) and the equivalent ``CAST(...)`` +form are declared interchangeable, so this asserts they parse to +structurally equal ASTs, not just that each round-trips individually. +""" + +from __future__ import annotations + +import pytest +import sqlglot + +import ossie_sql # noqa: F401 + + +@pytest.mark.parametrize( + "typed_literal,cast_form", + [ + ("DATE '2024-01-15'", "CAST('2024-01-15' AS DATE)"), + ("TIME '10:30:00'", "CAST('10:30:00' AS TIME)"), + ( + "TIMESTAMP_NTZ '2024-01-15 10:30:00'", + "CAST('2024-01-15 10:30:00' AS TIMESTAMP_NTZ)", + ), + ], +) +def test_typed_literal_equivalent_to_cast(typed_literal: str, cast_form: str) -> None: + a = sqlglot.parse_one(typed_literal, read="ossie") + b = sqlglot.parse_one(cast_form, read="ossie") + assert a == b + # Both spellings round-trip to the compact typed-literal form -- the + # spec's primary documented construction syntax for these types. + assert a.sql(dialect="ossie") == typed_literal + assert b.sql(dialect="ossie") == typed_literal + + +def test_try_cast_returns_null_on_failure_syntax() -> None: + parsed = sqlglot.parse_one("TRY_CAST(a AS INTEGER)", read="ossie") + assert isinstance(parsed, sqlglot.exp.TryCast) + # INTEGER / INT are spec-declared synonyms. + assert parsed.sql(dialect="ossie") == "TRY_CAST(a AS INT)" + + +@pytest.mark.parametrize( + "target_type", + ["VARCHAR", "INTEGER", "DECIMAL", "FLOAT", "BOOLEAN", "DATE", "TIMESTAMP", "TIME"], +) +def test_cast_supports_spec_target_types(target_type: str) -> None: + parsed = sqlglot.parse_one(f"CAST(a AS {target_type})", read="ossie") + assert isinstance(parsed, sqlglot.exp.Cast) diff --git a/core/python/tests/test_validate.py b/core/python/tests/test_validate.py new file mode 100644 index 00000000..2c24a7cb --- /dev/null +++ b/core/python/tests/test_validate.py @@ -0,0 +1,84 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Tests for ossie_sql.validate.validate_expression(). + +Covers the spec's "Not Supported in Expressions" table (SELECT/FROM/JOIN, +GROUP BY, WHERE, subqueries, CTEs, set operations, DDL/DML) plus the +pass-through-unknown-functions behavior from the "Dialect Extensions" +section. +""" + +from __future__ import annotations + +import pytest +import sqlglot + +import ossie_sql # noqa: F401 +from ossie_sql.validate import UnsupportedConstructError, validate_expression + +VALID_EXPRESSIONS = [ + "SUM(x)", + "CASE WHEN a THEN 1 ELSE 0 END", + "x IN (1, 2, 3)", + "amount / SUM(amount) OVER () * 100", + "x BETWEEN a AND b", + # Vendor/unknown function name: passes through, per the spec's default. + "SOME_VENDOR_SPECIFIC_FUNC(x, y)", + "EXISTS_IN(x)", +] + + +@pytest.mark.parametrize("source", VALID_EXPRESSIONS) +def test_valid_expressions_pass(source: str) -> None: + parsed = sqlglot.parse_one(source, read="ossie") + validate_expression(parsed) # must not raise + + +DISALLOWED_EXPRESSIONS = [ + "SELECT * FROM t", + "x IN (SELECT id FROM t)", + "WITH cte AS (SELECT 1) SELECT * FROM cte", + "SELECT 1 UNION SELECT 2", + "SELECT a FROM t1 JOIN t2 ON t1.id = t2.id", +] + + +@pytest.mark.parametrize("source", DISALLOWED_EXPRESSIONS) +def test_disallowed_constructs_raise(source: str) -> None: + parsed = sqlglot.parse_one(source, read="ossie") + with pytest.raises(UnsupportedConstructError): + validate_expression(parsed) + + +def test_groups_frame_mode_rejected() -> None: + parsed = sqlglot.parse_one( + "SUM(x) OVER (ORDER BY d GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)", + read="ossie", + ) + with pytest.raises(UnsupportedConstructError): + validate_expression(parsed) + + +def test_rows_and_range_frame_modes_accepted() -> None: + for frame in [ + "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW", + "ROWS BETWEEN 6 PRECEDING AND CURRENT ROW", + "RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW", + ]: + parsed = sqlglot.parse_one(f"SUM(x) OVER (ORDER BY d {frame})", read="ossie") + validate_expression(parsed) # must not raise diff --git a/core/python/uv.lock b/core/python/uv.lock new file mode 100644 index 00000000..d7bf4c06 --- /dev/null +++ b/core/python/uv.lock @@ -0,0 +1,322 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "apache-ossie-sql" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "sqlglot" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "sqlglot", specifier = ">=25.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.10" }, + { name = "pytest", specifier = ">=8.0" }, + { name = "ruff", specifier = ">=0.11" }, +] + +[[package]] +name = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" }, + { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" }, + { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" }, + { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" }, + { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" }, + { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/25/a6498964cfeec270c468cffdc118f69c29b412593610d55fa1327ca51ff4/librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082", size = 148029, upload-time = "2026-07-08T12:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/dc86d1bffd8e0c2818bace29d9f7783cfbb8e0673bf3673b5bbd5bbe0420/librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14", size = 153036, upload-time = "2026-07-08T12:24:47.257Z" }, + { url = "https://files.pythonhosted.org/packages/29/3f/b923826660f02f286186cd9303d52bb05ced0a13708edc104dc8480920e3/librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79", size = 493062, upload-time = "2026-07-08T12:24:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/6c0980a9c9b1302cb68d108906697b89eceb55889bb1dcf77c109aa56ca5/librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176", size = 485510, upload-time = "2026-07-08T12:24:49.727Z" }, + { url = "https://files.pythonhosted.org/packages/32/81/795ae3b9df5dd94079fb807e38191855e023e8c6249014ae6bc3f0d9a490/librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89", size = 515909, upload-time = "2026-07-08T12:24:51.135Z" }, + { url = "https://files.pythonhosted.org/packages/20/e5/182de15abce8907108a6fdb41487de65beb5099b74dc5841b19b099168db/librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f", size = 508620, upload-time = "2026-07-08T12:24:52.358Z" }, + { url = "https://files.pythonhosted.org/packages/32/03/33978d32db76e1f66377e8f78e42a2ca3c162143331677d1f50bbad36cfb/librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d", size = 530363, upload-time = "2026-07-08T12:24:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f5/b291fbd2d00f7d8287bcbf67b5aa0c6afed4bc26cef23e079629c47a2c04/librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd", size = 534209, upload-time = "2026-07-08T12:24:55.138Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/6f41f17939d191bc21609f220da8509316bc62797f078545fe83be522e78/librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588", size = 514254, upload-time = "2026-07-08T12:24:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/af/c2/2e4befa5410a7443019c14abccc94ff619797171f6b72013635fb87f31d7/librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1", size = 557611, upload-time = "2026-07-08T12:24:57.561Z" }, + { url = "https://files.pythonhosted.org/packages/ab/54/8b69f81448417adbc040a2185f4e2eece1e1994b7dcfaeed4662b30f98a5/librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21", size = 104906, upload-time = "2026-07-08T12:24:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/76/5a/f4aaf37b50f2fde12c8c663b83fdd499cdc24f957f19543d7414bfcc9e25/librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b", size = 125852, upload-time = "2026-07-08T12:25:00.065Z" }, + { url = "https://files.pythonhosted.org/packages/f2/99/bf1820e6feeabc2f218c24450ec0c995d6a91e8ba0fd3caf042c9e8adb2a/librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c", size = 111832, upload-time = "2026-07-08T12:25:01.148Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" }, + { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" }, + { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" }, + { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" }, + { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" }, + { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" }, + { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, +] + +[[package]] +name = "mypy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/b9/d75b3082b05f1b3028828aeb18e74ae5ab0a0936051bbf1f32f59f654747/mypy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329", size = 14838725, upload-time = "2026-07-13T11:32:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/50/79a65c6ea6e115bc73296038a4543b2d5c91f07912b918a2c616a2514bba/mypy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f", size = 13911128, upload-time = "2026-07-13T11:32:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/90/48/e11ed7716c26953ca321f726e452e374dbf81a6f2b8b212ec02af29b6b8f/mypy-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a", size = 14146742, upload-time = "2026-07-13T11:33:03.313Z" }, + { url = "https://files.pythonhosted.org/packages/06/72/6807565b1c4861ef66f7fdd98b51c61556356eab80235717b46c53bb8627/mypy-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36", size = 15081418, upload-time = "2026-07-13T11:31:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/00/80/1ea14c5d80e589e415973db3e47c78c2219a305b808b2b506395342c1d79/mypy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461", size = 15328164, upload-time = "2026-07-13T11:31:35.723Z" }, + { url = "https://files.pythonhosted.org/packages/37/28/8223157404a3d51920078459c37f80fbdc590e1d8ea049dc5ce48643022a/mypy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd", size = 11136472, upload-time = "2026-07-13T11:27:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cc/ea27e5959c5f258585a756b252031f3b313583d81b5064b2bebc41d3706b/mypy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568", size = 10135800, upload-time = "2026-07-13T11:30:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" }, + { url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" }, + { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" }, + { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" }, + { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" }, + { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" }, + { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" }, + { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" }, + { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, +] + +[[package]] +name = "sqlglot" +version = "30.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/cd/39a94f0f98076ee8e7c7c38fd4bba8d7845b0c629ff967057c64ef2c0989/sqlglot-30.14.0.tar.gz", hash = "sha256:df2ef5d2b8ca814313781f4ff35bf63e58f821ef517eeddbd523c19a61fa9bb9", size = 5944410, upload-time = "2026-07-27T11:23:30.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/ec/a729883ceda22dcd9117ce182f64d884bf494e72c4dfce00c2ad0a5978e1/sqlglot-30.14.0-py3-none-any.whl", hash = "sha256:fc768e24889d63a5e1237dea7ad305e5ffb4356a98b0bed828f89591ebcd3636", size = 719007, upload-time = "2026-07-27T11:23:28.637Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..b672448d --- /dev/null +++ b/mypy.ini @@ -0,0 +1,23 @@ +# Shared strict mypy rules for this repo's Python packages. +# +# mypy has no `extends`/`include` directive for config files, and a +# package's own pyproject.toml can't declaratively "point at" another +# config file the way ruff's `extend` does -- mypy only ever reads one +# config file per run, chosen by `--config-file` or fixed discovery in the +# current working directory (it doesn't search upward like ruff/eslint). +# +# So each package invokes mypy with this file explicit, e.g. from +# core/python/: +# +# uv run mypy --config-file ../../mypy.ini src tests +# +# Target paths are passed on the command line (not listed here as `files =`) +# so they're always resolved relative to the invoking package's own +# directory, regardless of where this shared file lives. +[mypy] +python_version = 3.11 +strict = True + +[mypy-sqlglot.*] +# sqlglot ships inline types but not a py.typed marker on all versions. +ignore_missing_imports = True diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..218a6f1d --- /dev/null +++ b/ruff.toml @@ -0,0 +1,20 @@ +# Shared ruff (lint + format) rules for this repo's Python packages. +# +# Individual packages opt in with: +# +# [tool.ruff] +# extend = "../../ruff.toml" # (relative path to this file) +# +# `extend` is ruff's native config-inheritance mechanism: the package's own +# `[tool.ruff]` settings are merged on top of this base (and can override +# it), rather than the usual "closest config wins outright" behavior. +line-length = 100 +target-version = "py311" + +[lint] +select = ["E", "F", "I", "N", "UP", "B", "A", "SIM"] + +[format] +# Explicit (not just relying on defaults): this is the formatting +# enforcement mechanism shared by every package that extends this file. +quote-style = "double"