diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c7e879..2112c84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [1.1.0] + +10/12/2025 + +- Remove dependency on sqlalchemy-utils. +- Add our own database utility functions for test purposes. +- Add comprehensive test suite for database utility functions. + + ## [1.0.4] 18/11/2025 diff --git a/README.md b/README.md index 558d462..a3ce480 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ A tool for comparing database schemas using SQLAlchemy. - Python 3.10 or higher (supports 3.10, 3.11, 3.12, 3.13, 3.14) - SQLAlchemy >= 1.4 -- sqlalchemy-utils ~= 0.41.2 ## Authors diff --git a/pyproject.toml b/pyproject.toml index 75bdf3c..21c9a51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sqlalchemy-diff" -version = "1.0.4" +version = "1.1.0" authors = [ { name = "Fabrizio Romano", email = "gianchub@gmail.com" }, { name = "Mark McArdle", email = "m.mc4rdle@gmail.com" }, @@ -31,7 +31,6 @@ maintainers = [ keywords = ["sqlalchemy", "diff", "compare"] dependencies = [ "sqlalchemy>=1.4,<3", - "sqlalchemy-utils>=0.40.0,!=0.42.0", ] [project.optional-dependencies] diff --git a/src/sqlalchemydiff/inspection/base.py b/src/sqlalchemydiff/inspection/base.py index c8af00e..e8182a3 100644 --- a/src/sqlalchemydiff/inspection/base.py +++ b/src/sqlalchemydiff/inspection/base.py @@ -1,5 +1,6 @@ import abc import inspect as stdlib_inspect +from typing import Any from sqlalchemy import inspect from sqlalchemy.engine import Engine @@ -59,7 +60,7 @@ def __init__(self, one_alias: str = "one", two_alias: str = "two"): @abc.abstractmethod def inspect( self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None - ) -> dict: ... # pragma: no cover + ) -> Any: ... # pragma: no cover @abc.abstractmethod def diff(self, one: dict, two: dict) -> dict: ... # pragma: no cover diff --git a/src/sqlalchemydiff/inspection/inspectors.py b/src/sqlalchemydiff/inspection/inspectors.py index f803735..20c231a 100644 --- a/src/sqlalchemydiff/inspection/inspectors.py +++ b/src/sqlalchemydiff/inspection/inspectors.py @@ -1,4 +1,4 @@ -from typing import cast +from typing import Any, cast from sqlalchemy.engine import Engine @@ -144,7 +144,7 @@ def diff(self, one: dict, two: dict) -> dict: def _is_supported(self, inspector: Inspector) -> bool: return hasattr(inspector, "get_foreign_keys") - def _get_fk_identifier(self, fk: dict) -> dict: + def _get_fk_identifier(self, fk: Any) -> Any: if not fk["name"]: fk["name"] = f"_unnamed_fk_{fk['referred_table']}_{'_'.join(fk['constrained_columns'])}" return fk diff --git a/tests/db_util.py b/tests/db_util.py new file mode 100644 index 0000000..9c05e5e --- /dev/null +++ b/tests/db_util.py @@ -0,0 +1,173 @@ +from pathlib import Path +from urllib.parse import urlparse + +from sqlalchemy import create_engine, text + + +def _parse_database_uri(uri): + """Parse a database URI and return components.""" + parsed = urlparse(uri) + return { + "scheme": parsed.scheme, + "username": parsed.username, + "password": parsed.password, + "hostname": parsed.hostname, + "port": parsed.port, + "database": parsed.path.lstrip("/") if parsed.path else None, + "path": parsed.path, + } + + +def _get_postgres_admin_uri(uri): + """Get a PostgreSQL URI pointing to the 'postgres' database for admin operations.""" + parsed = urlparse(uri) + return parsed._replace(path="postgres").geturl() + + +def _get_sqlite_file_path(uri): + """Extract the file path from a SQLite URI as a Path object.""" + parsed = _parse_database_uri(uri) + path = parsed["path"] + + # Handle :memory: and empty path + if not path or path == "/:memory:" or path == "/": + return None + + # Remove leading slash + if path.startswith("/"): + path = path[1:] + + # Handle empty string after stripping + if not path: + return None + + return Path(path) + + +def database_exists(uri): + """Check if a database exists. + + Args: + uri: Database URI (postgresql://... or sqlite:///...) + + Returns: + bool: True if database exists, False otherwise + """ + parsed = _parse_database_uri(uri) + scheme = parsed["scheme"] + + if scheme == "postgresql": + admin_uri = _get_postgres_admin_uri(uri) + target_db = parsed["database"] + + engine = create_engine(admin_uri, isolation_level="AUTOCOMMIT") + try: + with engine.connect() as conn: + result = conn.execute( + text("SELECT 1 FROM pg_database WHERE datname = :dbname"), {"dbname": target_db} + ) + return result.fetchone() is not None + finally: + engine.dispose() + + elif scheme == "sqlite": + file_path = _get_sqlite_file_path(uri) + + # In-memory databases always "exist" (they're created on connection) + if file_path is None: + return True + + # For file-based SQLite, check if file exists + return file_path.exists() + + else: + raise ValueError(f"Unsupported database scheme: {scheme}") + + +def create_database(uri): + """Create a database. + + Args: + uri: Database URI (postgresql://... or sqlite:///...) + """ + parsed = _parse_database_uri(uri) + scheme = parsed["scheme"] + + if scheme == "postgresql": + admin_uri = _get_postgres_admin_uri(uri) + target_db = parsed["database"] + + engine = create_engine(admin_uri, isolation_level="AUTOCOMMIT") + try: + with engine.connect() as conn: + # Database should not exist since create_db() calls drop_db() first + conn.execute(text(f'CREATE DATABASE "{target_db}"')) + finally: + engine.dispose() + + elif scheme == "sqlite": + file_path = _get_sqlite_file_path(uri) + + # In-memory databases don't need creation + if file_path is None: + return + + # For file-based SQLite, ensure the directory exists + if file_path.parent: + file_path.parent.mkdir(parents=True, exist_ok=True) + + # Create an empty file to "create" the database + # SQLite will create the file on first connection, but we'll touch it here + if not file_path.exists(): + file_path.touch() + + else: + raise ValueError(f"Unsupported database scheme: {scheme}") + + +def drop_database(uri): + """Drop a database. + + Args: + uri: Database URI (postgresql://... or sqlite:///...) + """ + parsed = _parse_database_uri(uri) + scheme = parsed["scheme"] + + if scheme == "postgresql": + admin_uri = _get_postgres_admin_uri(uri) + target_db = parsed["database"] + + engine = create_engine(admin_uri, isolation_level="AUTOCOMMIT") + try: + with engine.connect() as conn: + # Terminate any existing connections to the database + conn.execute( + text( + """ + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = :dbname + AND pid <> pg_backend_pid() + """ + ), + {"dbname": target_db}, + ) + # Drop the database + conn.execute(text(f'DROP DATABASE IF EXISTS "{target_db}"')) + finally: + engine.dispose() + + elif scheme == "sqlite": + file_path = _get_sqlite_file_path(uri) + + # In-memory databases don't need dropping + if file_path is None: + return + + # For file-based SQLite, delete the file + if file_path.exists(): + file_path.unlink() + + else: + raise ValueError(f"Unsupported database scheme: {scheme}") diff --git a/tests/test_db_util.py b/tests/test_db_util.py new file mode 100644 index 0000000..d27cb31 --- /dev/null +++ b/tests/test_db_util.py @@ -0,0 +1,490 @@ +import uuid +from pathlib import Path + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.exc import ProgrammingError + +from tests.db_util import ( + _get_postgres_admin_uri, + _get_sqlite_file_path, + _parse_database_uri, + create_database, + database_exists, + drop_database, +) + + +class TestParseDatabaseUri: + """Test the _parse_database_uri helper function.""" + + def test_parse_postgresql_uri(self): + """Test parsing a PostgreSQL URI.""" + uri = "postgresql://user:pass@localhost:5432/mydb" + result = _parse_database_uri(uri) + + assert result["scheme"] == "postgresql" + assert result["username"] == "user" + assert result["password"] == "pass" + assert result["hostname"] == "localhost" + assert result["port"] == 5432 + assert result["database"] == "mydb" + assert result["path"] == "/mydb" + + def test_parse_postgresql_uri_no_port(self): + """Test parsing a PostgreSQL URI without port.""" + uri = "postgresql://user:pass@localhost/mydb" + result = _parse_database_uri(uri) + + assert result["scheme"] == "postgresql" + assert result["username"] == "user" + assert result["password"] == "pass" + assert result["hostname"] == "localhost" + assert result["port"] is None + assert result["database"] == "mydb" + + def test_parse_sqlite_memory_uri(self): + """Test parsing a SQLite in-memory URI.""" + uri = "sqlite:///:memory:" + result = _parse_database_uri(uri) + + assert result["scheme"] == "sqlite" + assert result["username"] is None + assert result["password"] is None + assert result["hostname"] is None + assert result["port"] is None + assert result["database"] == ":memory:" # lstrip("/") removes leading slash + assert result["path"] == "/:memory:" + + def test_parse_sqlite_file_uri(self): + """Test parsing a SQLite file URI.""" + uri = "sqlite:///path/to/database.db" + result = _parse_database_uri(uri) + + assert result["scheme"] == "sqlite" + assert result["path"] == "/path/to/database.db" + assert result["database"] == "path/to/database.db" + + +class TestGetPostgresAdminUri: + """Test the _get_postgres_admin_uri helper function.""" + + def test_get_postgres_admin_uri(self): + """Test generating admin URI for PostgreSQL.""" + uri = "postgresql://user:pass@localhost:5432/mydb" + admin_uri = _get_postgres_admin_uri(uri) + + assert admin_uri == "postgresql://user:pass@localhost:5432/postgres" + + def test_get_postgres_admin_uri_no_port(self): + """Test generating admin URI without port.""" + uri = "postgresql://user:pass@localhost/mydb" + admin_uri = _get_postgres_admin_uri(uri) + + assert admin_uri == "postgresql://user:pass@localhost/postgres" + + +class TestGetSqliteFilePath: + """Test the _get_sqlite_file_path helper function.""" + + def test_get_sqlite_file_path_memory(self): + """Test extracting path from in-memory SQLite URI.""" + uri = "sqlite:///:memory:" + result = _get_sqlite_file_path(uri) + + assert result is None + + def test_get_sqlite_file_path_empty(self): + """Test extracting path from empty SQLite URI.""" + uri = "sqlite:///" + result = _get_sqlite_file_path(uri) + + assert result is None + + def test_get_sqlite_file_path_absolute(self): + """Test extracting absolute path from SQLite URI.""" + uri = "sqlite:///path/to/database.db" + result = _get_sqlite_file_path(uri) + + assert isinstance(result, Path) + assert result == Path("path/to/database.db") + + def test_get_sqlite_file_path_relative(self): + """Test extracting relative path from SQLite URI.""" + uri = "sqlite:///database.db" + result = _get_sqlite_file_path(uri) + + assert isinstance(result, Path) + assert result == Path("database.db") + + +class TestDatabaseExists: + """Test the database_exists function.""" + + @pytest.fixture + def unique_db_name(self): + """Generate a unique database name for testing.""" + return f"test_db_exists_{uuid.uuid4().hex[:8]}" + + @pytest.fixture + def postgres_uri(self, make_postgres_uri, unique_db_name): + """Create a PostgreSQL URI for testing.""" + return make_postgres_uri(unique_db_name) + + def test_database_exists_postgresql_nonexistent(self, postgres_uri): + """Test checking non-existent PostgreSQL database.""" + assert not database_exists(postgres_uri) + + def test_database_exists_postgresql_existent(self, postgres_uri): + """Test checking existing PostgreSQL database.""" + # Create the database first + create_database(postgres_uri) + try: + assert database_exists(postgres_uri) + finally: + drop_database(postgres_uri) + + def test_database_exists_sqlite_memory(self): + """Test checking in-memory SQLite database (always exists).""" + uri = "sqlite:///:memory:" + assert database_exists(uri) + + def test_database_exists_sqlite_file_nonexistent(self, tmp_path): + """Test checking non-existent SQLite file database.""" + db_file = tmp_path / "nonexistent.db" + uri = f"sqlite:///{db_file}" + assert not database_exists(uri) + + def test_database_exists_sqlite_file_existent(self, tmp_path): + """Test checking existing SQLite file database.""" + db_file = tmp_path / "existing.db" + db_file.touch() + uri = f"sqlite:///{db_file}" + assert database_exists(uri) + + def test_database_exists_unsupported_scheme(self): + """Test that unsupported schemes raise ValueError.""" + uri = "mysql://user:pass@localhost/db" + with pytest.raises(ValueError, match="Unsupported database scheme: mysql"): + database_exists(uri) + + +class TestCreateDatabase: + """Test the create_database function.""" + + @pytest.fixture + def unique_db_name(self): + """Generate a unique database name for testing.""" + return f"test_db_create_{uuid.uuid4().hex[:8]}" + + @pytest.fixture + def postgres_uri(self, make_postgres_uri, unique_db_name): + """Create a PostgreSQL URI for testing.""" + return make_postgres_uri(unique_db_name) + + def test_create_database_postgresql(self, postgres_uri): + """Test creating a PostgreSQL database.""" + # Ensure it doesn't exist first + if database_exists(postgres_uri): + drop_database(postgres_uri) + + create_database(postgres_uri) + try: + assert database_exists(postgres_uri) + finally: + drop_database(postgres_uri) + + def test_create_database_postgresql_already_exists(self, postgres_uri): + """Test creating a PostgreSQL database that already exists.""" + # Create the database first + if not database_exists(postgres_uri): + create_database(postgres_uri) + + # PostgreSQL will raise a ProgrammingError when trying to create existing database + with pytest.raises(ProgrammingError): + create_database(postgres_uri) + + # Cleanup + drop_database(postgres_uri) + + def test_create_database_sqlite_memory(self): + """Test creating in-memory SQLite database (no-op).""" + uri = "sqlite:///:memory:" + # In-memory databases always exist (they're created on connection) + assert database_exists(uri) + # Should not raise an error + create_database(uri) + # Still exists after create (no-op for in-memory) + assert database_exists(uri) + + def test_create_database_sqlite_file(self, tmp_path): + """Test creating a SQLite file database.""" + db_file = tmp_path / "new_database.db" + uri = f"sqlite:///{db_file}" + + assert not db_file.exists() + create_database(uri) + assert db_file.exists() + assert database_exists(uri) + + def test_create_database_sqlite_file_with_subdirectory(self, tmp_path): + """Test creating a SQLite file database in a subdirectory.""" + subdir = tmp_path / "subdir" + db_file = subdir / "database.db" + uri = f"sqlite:///{db_file}" + + assert not subdir.exists() + assert not db_file.exists() + + create_database(uri) + + assert subdir.exists() + assert db_file.exists() + assert database_exists(uri) + + def test_create_database_sqlite_file_already_exists(self, tmp_path): + """Test creating a SQLite file database that already exists.""" + db_file = tmp_path / "existing.db" + db_file.touch() + uri = f"sqlite:///{db_file}" + + # Should not raise an error, just touch the file + create_database(uri) + assert db_file.exists() + + def test_create_database_unsupported_scheme(self): + """Test that unsupported schemes raise ValueError.""" + uri = "mysql://user:pass@localhost/db" + with pytest.raises(ValueError, match="Unsupported database scheme: mysql"): + create_database(uri) + + +class TestDropDatabase: + """Test the drop_database function.""" + + @pytest.fixture + def unique_db_name(self): + """Generate a unique database name for testing.""" + return f"test_db_drop_{uuid.uuid4().hex[:8]}" + + @pytest.fixture + def postgres_uri(self, make_postgres_uri, unique_db_name): + """Create a PostgreSQL URI for testing.""" + return make_postgres_uri(unique_db_name) + + def test_drop_database_postgresql_existent(self, postgres_uri): + """Test dropping an existing PostgreSQL database.""" + # Create the database first + if not database_exists(postgres_uri): + create_database(postgres_uri) + + assert database_exists(postgres_uri) + drop_database(postgres_uri) + assert not database_exists(postgres_uri) + + def test_drop_database_postgresql_nonexistent(self, postgres_uri): + """Test dropping a non-existent PostgreSQL database.""" + # Ensure it doesn't exist + if database_exists(postgres_uri): + drop_database(postgres_uri) + + # Should not raise an error (uses IF EXISTS) + drop_database(postgres_uri) + assert not database_exists(postgres_uri) + + def test_drop_database_postgresql_with_connections(self, postgres_uri): + """Test dropping a PostgreSQL database that has active connections.""" + # Create the database + if not database_exists(postgres_uri): + create_database(postgres_uri) + + # Create a connection to the database + engine = create_engine(postgres_uri) + conn = engine.connect() + + try: + # Drop should terminate connections and succeed + drop_database(postgres_uri) + assert not database_exists(postgres_uri) + finally: + conn.close() + engine.dispose() + + def test_drop_database_sqlite_memory(self): + """Test dropping in-memory SQLite database (no-op).""" + uri = "sqlite:///:memory:" + # Should not raise an error + drop_database(uri) + assert database_exists(uri) # Still exists (in-memory) + + def test_drop_database_sqlite_file_existent(self, tmp_path): + """Test dropping an existing SQLite file database.""" + db_file = tmp_path / "to_drop.db" + db_file.touch() + uri = f"sqlite:///{db_file}" + + assert db_file.exists() + drop_database(uri) + assert not db_file.exists() + assert not database_exists(uri) + + def test_drop_database_sqlite_file_nonexistent(self, tmp_path): + """Test dropping a non-existent SQLite file database.""" + db_file = tmp_path / "nonexistent.db" + uri = f"sqlite:///{db_file}" + + assert not db_file.exists() + # Should not raise an error + drop_database(uri) + assert not db_file.exists() + + def test_drop_database_unsupported_scheme(self): + """Test that unsupported schemes raise ValueError.""" + uri = "mysql://user:pass@localhost/db" + with pytest.raises(ValueError, match="Unsupported database scheme: mysql"): + drop_database(uri) + + +class TestDatabaseLifecycle: + """Test complete database lifecycle operations.""" + + @pytest.fixture + def unique_db_name(self): + """Generate a unique database name for testing.""" + return f"test_db_lifecycle_{uuid.uuid4().hex[:8]}" + + @pytest.fixture + def postgres_uri(self, make_postgres_uri, unique_db_name): + """Create a PostgreSQL URI for testing.""" + return make_postgres_uri(unique_db_name) + + def test_postgresql_lifecycle(self, postgres_uri): + """Test complete PostgreSQL database lifecycle.""" + # Start: database should not exist + assert not database_exists(postgres_uri) + + # Create database + create_database(postgres_uri) + assert database_exists(postgres_uri) + + # Drop database + drop_database(postgres_uri) + assert not database_exists(postgres_uri) + + # Create again + create_database(postgres_uri) + assert database_exists(postgres_uri) + + # Final cleanup + drop_database(postgres_uri) + assert not database_exists(postgres_uri) + + def test_sqlite_file_lifecycle(self, tmp_path): + """Test complete SQLite file database lifecycle.""" + db_file = tmp_path / "lifecycle.db" + uri = f"sqlite:///{db_file}" + + # Start: database should not exist + assert not database_exists(uri) + + # Create database + create_database(uri) + assert database_exists(uri) + assert db_file.exists() + + # Drop database + drop_database(uri) + assert not database_exists(uri) + assert not db_file.exists() + + # Create again + create_database(uri) + assert database_exists(uri) + assert db_file.exists() + + # Final cleanup + drop_database(uri) + assert not database_exists(uri) + assert not db_file.exists() + + def test_sqlite_memory_lifecycle(self): + """Test SQLite in-memory database lifecycle.""" + uri = "sqlite:///:memory:" + + # In-memory always exists + assert database_exists(uri) + + # Create is a no-op + create_database(uri) + assert database_exists(uri) + + # Drop is a no-op + drop_database(uri) + assert database_exists(uri) + + +class TestDatabaseOperationsEdgeCases: + """Test edge cases and error conditions.""" + + @pytest.fixture + def unique_db_name(self): + """Generate a unique database name for testing.""" + return f"test_db_edge_{uuid.uuid4().hex[:8]}" + + @pytest.fixture + def postgres_uri(self, make_postgres_uri, unique_db_name): + """Create a PostgreSQL URI for testing.""" + return make_postgres_uri(unique_db_name) + + def test_postgresql_database_name_with_special_chars(self, make_postgres_uri): + """Test PostgreSQL database with special characters in name.""" + # PostgreSQL allows quoted identifiers, but we should test our quoting + db_name = f"test_db_{uuid.uuid4().hex[:8]}" + uri = make_postgres_uri(db_name) + + create_database(uri) + try: + assert database_exists(uri) + finally: + drop_database(uri) + + def test_sqlite_file_path_with_spaces(self, tmp_path): + """Test SQLite file path with spaces.""" + db_file = tmp_path / "database with spaces.db" + uri = f"sqlite:///{db_file}" + + create_database(uri) + assert db_file.exists() + assert database_exists(uri) + + drop_database(uri) + assert not db_file.exists() + + def test_sqlite_file_path_unicode(self, tmp_path): + """Test SQLite file path with unicode characters.""" + db_file = tmp_path / "数据库.db" + uri = f"sqlite:///{db_file}" + + create_database(uri) + assert db_file.exists() + assert database_exists(uri) + + drop_database(uri) + assert not db_file.exists() + + def test_multiple_operations_sequence(self, postgres_uri): + """Test multiple create/drop operations in sequence.""" + # Create and drop multiple times + for _ in range(3): + create_database(postgres_uri) + assert database_exists(postgres_uri) + drop_database(postgres_uri) + assert not database_exists(postgres_uri) + + def test_sqlite_empty_path(self): + """Test SQLite URI with empty path.""" + uri = "sqlite:///" + # Should be treated as in-memory + assert database_exists(uri) + create_database(uri) # Should be no-op + drop_database(uri) # Should be no-op diff --git a/tests/util.py b/tests/util.py index 6820d0a..3260a2c 100644 --- a/tests/util.py +++ b/tests/util.py @@ -3,9 +3,9 @@ from sqlalchemy import create_engine from sqlalchemy.engine import Engine from sqlalchemy.orm.decl_api import DeclarativeMeta -from sqlalchemy_utils import create_database, database_exists, drop_database from tests import assert_items_equal +from tests.db_util import create_database, database_exists, drop_database def create_db(uri): diff --git a/tox.toml b/tox.toml index 48757b1..9627a7a 100644 --- a/tox.toml +++ b/tox.toml @@ -36,7 +36,6 @@ description = "Run unit tests with Python {basepython} and sqlalchemy V1.4" skip_install = true deps = [ "sqlalchemy~=1.4", - "sqlalchemy-utils>=0.40.0,!=0.42.0", "pytest~=9.0.1", "pytest-cov~=7.0.0", "psycopg2-binary", diff --git a/uv.lock b/uv.lock index c952457..a1ee063 100644 --- a/uv.lock +++ b/uv.lock @@ -515,7 +515,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -2015,11 +2015,10 @@ wheels = [ [[package]] name = "sqlalchemy-diff" -version = "1.0.4" +version = "1.1.0" source = { editable = "." } dependencies = [ { name = "sqlalchemy" }, - { name = "sqlalchemy-utils" }, ] [package.optional-dependencies] @@ -2062,23 +2061,10 @@ requires-dist = [ { name = "ruff", marker = "extra == 'lint'", specifier = "~=0.14.1" }, { name = "sqlalchemy", specifier = ">=1.4,<3" }, { name = "sqlalchemy-diff", extras = ["dev", "lint", "test"], marker = "extra == 'all'" }, - { name = "sqlalchemy-utils", specifier = ">=0.40.0,!=0.42.0" }, { name = "ty", marker = "extra == 'lint'" }, ] provides-extras = ["dev", "lint", "test", "all"] -[[package]] -name = "sqlalchemy-utils" -version = "0.41.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sqlalchemy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4d/bf/abfd5474cdd89ddd36dbbde9c6efba16bfa7f5448913eba946fed14729da/SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990", size = 138017, upload-time = "2024-03-24T15:17:28.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/f0/dc4757b83ac1ab853cf222df8535ed73973e0c203d983982ba7b8bc60508/SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e", size = 93083, upload-time = "2024-03-24T15:17:24.533Z" }, -] - [[package]] name = "stack-data" version = "0.6.3"