From 7d0eca29f67e2bb6800eb83e897e74532d98eef9 Mon Sep 17 00:00:00 2001 From: Jagat Singh Date: Fri, 5 Sep 2025 22:32:36 +0530 Subject: [PATCH 1/5] Add uv support for modern Python dependency management - Add pyproject.toml with uv configuration and project metadata - Update CI buildspec to use uv instead of tox - Update README with uv installation and usage instructions - Maintain backward compatibility with existing tox workflow --- README.rst | 19 ++++++---- ci/build-spec.yml | 5 ++- pyproject.toml | 96 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 pyproject.toml diff --git a/README.rst b/README.rst index 1693649b..a0933969 100644 --- a/README.rst +++ b/README.rst @@ -37,11 +37,18 @@ for details on Redshift-specific features the dialect supports. Running Tests ------------- -Tests are ran via tox and can be run with the following command:: +This project uses `uv `_ for dependency management and testing. + +To install dependencies and run tests:: + + $ uv sync --dev + $ uv run pytest tests/ + +Alternatively, you can still use tox:: $ tox -However, this will not run integration tests unless the following +However, integration tests will not run unless the following environment variables are set: * REDSHIFT_HOST @@ -78,11 +85,9 @@ also the following settings:: To perform a release, run the following:: - python -m venv ~/.virtualenvs/dist - workon dist - pip install -U pip setuptools wheel - pip install -U tox zest.releaser - fullrelease # follow prompts, use semver ish with versions. + uv sync --dev + uv add zest.releaser + uv run fullrelease # follow prompts, use semver ish with versions. The releaser will handle updating version data on the package and in CHANGES.rst along with tagging the repo and uploading to PyPI. diff --git a/ci/build-spec.yml b/ci/build-spec.yml index a0dddc79..ba8927d9 100644 --- a/ci/build-spec.yml +++ b/ci/build-spec.yml @@ -26,7 +26,8 @@ phases: - cd $orig_dir pre_build: commands: - - pip install tox + - pip install uv + - uv sync --dev build: commands: - - tox $TEST_COMMAND + - uv run pytest tests/ --dbdriver psycopg2 --dbdriver psycopg2cffi --dbdriver redshift_connector $TEST_COMMAND diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..6a1c719f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,96 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "sqlalchemy-redshift" +version = "0.8.15.dev0" +description = "Amazon Redshift Dialect for sqlalchemy" +readme = {file = "README.rst", content-type = "text/x-rst"} +license = {text = "MIT"} +authors = [ + {name = "Matt George", email = "mgeorge@gmail.com"}, +] +maintainers = [ + {name = "Thomas Grainger", email = "sqlalchemy-redshift@graingert.co.uk"}, +] +requires-python = ">=3.4" +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +dependencies = [ + "SQLAlchemy>=0.9.2,<2.0.0", + "packaging", +] + +[project.urls] +Homepage = "https://github.com/sqlalchemy-redshift/sqlalchemy-redshift" +Repository = "https://github.com/sqlalchemy-redshift/sqlalchemy-redshift" + +[project.entry-points."sqlalchemy.dialects"] +redshift = "sqlalchemy_redshift.dialect:RedshiftDialect_psycopg2" +"redshift.psycopg2" = "sqlalchemy_redshift.dialect:RedshiftDialect_psycopg2" +"redshift.psycopg2cffi" = "sqlalchemy_redshift.dialect:RedshiftDialect_psycopg2cffi" +"redshift.redshift_connector" = "sqlalchemy_redshift.dialect:RedshiftDialect_redshift_connector" + +[tool.setuptools.packages.find] +include = ["sqlalchemy_redshift*", "redshift_sqlalchemy*"] + +[tool.setuptools.package-data] +sqlalchemy_redshift = ["redshift-ca-bundle.crt"] + +[project.optional-dependencies] +dev = [ + "alembic==1.9.2", + "packaging==20.4", + "psycopg2==2.8.6", + "psycopg2cffi==2.8.1", + "pytest==7.2.1", + "requests==2.25.0", + "redshift_connector==2.0.907", + "flake8==4.0.1", +] +test = [ + "alembic==1.9.2", + "packaging==20.4", + "psycopg2==2.8.6", + "psycopg2cffi==2.8.1", + "pytest==7.2.1", + "requests==2.25.0", + "redshift_connector==2.0.907", +] +docs = [ + "sphinx==1.6.3", + "numpydoc==0.6.0", + "psycopg2-binary==2.9.1", + "jinja2<3.1.0", +] + +[tool.pytest.ini_options] +addopts = "--doctest-modules --doctest-glob='*.rst' --ignore=setup.py --ignore=docs/conf.py" +doctest_optionflags = "NORMALIZE_WHITESPACE" + +[tool.uv] +dev-dependencies = [ + "alembic==1.9.2", + "packaging==20.4", + "psycopg2==2.8.6", + "psycopg2cffi==2.8.1", + "pytest==7.2.1", + "requests==2.25.0", + "redshift_connector==2.0.907", + "flake8==4.0.1", +] \ No newline at end of file From 572e229f1554cdd9a32c6385c8339007bff7fe82 Mon Sep 17 00:00:00 2001 From: Jagat Singh Date: Fri, 5 Sep 2025 23:20:47 +0530 Subject: [PATCH 2/5] Update SqlAlchemy and Python versions --- .github/ISSUE_TEMPLATE/bug_report.yml | 138 ++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 11 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 138 ++++++++++++++++++ .github/ISSUE_TEMPLATE/question.yml | 66 +++++++++ .github/SECURITY.md | 62 ++++++++ .github/dependabot.yml | 39 +++++ .github/labels.yml | 150 +++++++++++++++++++ .github/pull_request_template.md | 73 ++++++++++ .github/workflows/codeql.yml | 53 +++++++ .github/workflows/label-sync.yml | 26 ++++ .github/workflows/release.yml | 158 +++++++++++++++++++++ .github/workflows/security.yml | 71 +++++++++ .github/workflows/stale.yml | 56 ++++++++ .gitignore | 6 +- MANIFEST.in | 7 - README.rst | 6 +- buildspec_test.yml | 52 +++++++ ci/build-spec.yml | 4 +- ci/ci-pipeline.yml | 70 +++++---- pyproject.toml | 70 +++++---- redshift_sqlalchemy/__init__.py | 22 --- requirements-docs.txt | 5 - setup.cfg | 2 - setup.py | 51 ------- sqlalchemy_redshift/__init__.py | 13 +- sqlalchemy_redshift/ddl.py | 2 +- sqlalchemy_redshift/dialect.py | 123 ++++++++++++---- tests/rs_sqla_test_utils/models.py | 4 +- tests/test_compiler.py | 2 +- tests/test_copy_command.py | 11 +- tests/test_default_ssl.py | 4 +- tests/test_delete_stmt.py | 14 +- tests/test_dialects.py | 2 +- tests/test_materialized_views.py | 8 +- tests/test_unload_from_select.py | 24 ++-- tox.ini | 42 ------ 36 files changed, 1308 insertions(+), 277 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/question.yml create mode 100644 .github/SECURITY.md create mode 100644 .github/dependabot.yml create mode 100644 .github/labels.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/label-sync.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/security.yml create mode 100644 .github/workflows/stale.yml delete mode 100644 MANIFEST.in create mode 100644 buildspec_test.yml delete mode 100644 redshift_sqlalchemy/__init__.py delete mode 100644 requirements-docs.txt delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 tox.ini diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..7b746309 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,138 @@ +name: ๐Ÿ› Bug Report +description: Report a bug or unexpected behavior in sqlalchemy-redshift +title: "[Bug]: " +labels: ["bug", "needs-triage"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! Please fill out the information below to help us diagnose and fix the issue. + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission checklist + options: + - label: I have searched existing issues to ensure this bug has not been reported before + required: true + - label: I have read the documentation and confirmed this is not expected behavior + required: true + - label: I am using a supported version of sqlalchemy-redshift (0.8.x) + required: true + + - type: textarea + id: description + attributes: + label: Bug Description + description: A clear and concise description of what the bug is. + placeholder: Describe the bug here... + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to Reproduce + description: Steps to reproduce the behavior + placeholder: | + 1. Import sqlalchemy_redshift + 2. Create engine with '...' + 3. Execute query '...' + 4. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened instead? + validations: + required: true + + - type: textarea + id: code + attributes: + label: Code Sample + description: Please provide a minimal, reproducible code sample + render: python + placeholder: | + import sqlalchemy as sa + from sqlalchemy_redshift import dialect + + # Your code here... + + - type: textarea + id: error + attributes: + label: Error Output + description: If applicable, paste the full error message and traceback + render: text + placeholder: Paste error output here... + + - type: dropdown + id: python_version + attributes: + label: Python Version + description: What version of Python are you using? + options: + - "3.11" + - "3.12" + - "3.13" + - "Other (please specify in additional context)" + validations: + required: true + + - type: input + id: sqlalchemy_redshift_version + attributes: + label: sqlalchemy-redshift Version + description: What version of sqlalchemy-redshift are you using? + placeholder: "0.8.14" + validations: + required: true + + - type: input + id: sqlalchemy_version + attributes: + label: SQLAlchemy Version + description: What version of SQLAlchemy are you using? + placeholder: "2.0.43" + validations: + required: true + + - type: input + id: redshift_version + attributes: + label: Redshift Version/Type + description: Are you using Redshift Cluster, Serverless, or something else? + placeholder: "Redshift Serverless" + + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you using? + options: + - "Linux" + - "macOS" + - "Windows" + - "Other (please specify in additional context)" + validations: + required: true + + - type: textarea + id: additional_context + attributes: + label: Additional Context + description: Add any other context about the problem here, including configuration details, environment setup, etc. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..417f181e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: ๐Ÿ“š Documentation + url: https://sqlalchemy-redshift.readthedocs.io/ + about: Check the documentation for usage examples and API reference + - name: ๐Ÿ’ฌ Discussions + url: https://github.com/sqlalchemy-redshift/sqlalchemy-redshift/discussions + about: Ask questions and discuss ideas with the community + - name: ๐Ÿ”’ Security Issues + url: https://github.com/sqlalchemy-redshift/sqlalchemy-redshift/security/advisories/new + about: Report security vulnerabilities privately \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..1d7955eb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,138 @@ +name: โœจ Feature Request +description: Suggest a new feature or enhancement for sqlalchemy-redshift +title: "[Feature]: " +labels: ["enhancement", "needs-triage"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to suggest a feature! Please provide as much detail as possible to help us understand and evaluate your request. + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission checklist + options: + - label: I have searched existing issues to ensure this feature has not been requested before + required: true + - label: I have read the documentation to confirm this functionality doesn't already exist + required: true + - label: This feature request is related to sqlalchemy-redshift functionality + required: true + + - type: textarea + id: summary + attributes: + label: Feature Summary + description: A brief, clear summary of the feature you'd like to see + placeholder: What feature would you like to see added? + validations: + required: true + + - type: textarea + id: problem + attributes: + label: Problem Statement + description: What problem does this feature solve? What is the current limitation? + placeholder: | + Describe the problem or limitation you're facing... + + For example: + - "Currently, there's no way to..." + - "When working with X, it's difficult to..." + - "Users have to manually..." + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: Describe the solution you'd like to see implemented + placeholder: | + Describe your preferred solution in detail... + + For example: + - "Add a new method/function that..." + - "Extend the existing X to support..." + - "Provide a configuration option for..." + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternative Solutions + description: Have you considered any alternative solutions or workarounds? + placeholder: | + Describe any alternative solutions you've considered... + + Current workarounds you might be using... + + - type: dropdown + id: priority + attributes: + label: Priority + description: How important is this feature to you? + options: + - "Low - Nice to have" + - "Medium - Would be helpful" + - "High - Important for my use case" + - "Critical - Blocking my work" + validations: + required: true + + - type: dropdown + id: complexity + attributes: + label: Estimated Complexity + description: How complex do you think this feature would be to implement? + options: + - "Low - Small change or addition" + - "Medium - Moderate development effort" + - "High - Significant change or new functionality" + - "I'm not sure" + + - type: textarea + id: use_case + attributes: + label: Use Case + description: Describe your specific use case and how this feature would help + placeholder: | + Provide details about your specific use case: + - What are you trying to accomplish? + - How would this feature fit into your workflow? + - Are there other users who would benefit from this? + + - type: textarea + id: example + attributes: + label: Code Example + description: If applicable, provide a code example showing how you'd like to use this feature + render: python + placeholder: | + # Example of how the feature might be used + import sqlalchemy as sa + from sqlalchemy_redshift import dialect + + # Your example code here... + + - type: dropdown + id: contribution + attributes: + label: Contribution + description: Would you be interested in contributing to the implementation of this feature? + options: + - "Yes, I'd like to work on this" + - "Yes, I could help with testing" + - "Yes, I could help with documentation" + - "Maybe, depending on complexity" + - "No, I'm not able to contribute" + + - type: textarea + id: additional_context + attributes: + label: Additional Context + description: Add any other context, links, or screenshots about the feature request here \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 00000000..57c1d882 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,66 @@ +name: โ“ Question +description: Ask a question about using sqlalchemy-redshift +title: "[Question]: " +labels: ["question", "needs-triage"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Have a question about using sqlalchemy-redshift? We're here to help! + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission checklist + options: + - label: I have read the documentation and couldn't find the answer + required: true + - label: I have searched existing issues and discussions + required: true + + - type: textarea + id: question + attributes: + label: Question + description: What would you like to know? + placeholder: Ask your question here... + validations: + required: true + + - type: textarea + id: context + attributes: + label: Context + description: Provide any relevant context about what you're trying to accomplish + placeholder: | + What are you trying to do? + What have you already tried? + Any relevant background information? + + - type: textarea + id: code + attributes: + label: Code Sample + description: If applicable, provide a code sample related to your question + render: python + placeholder: | + import sqlalchemy as sa + from sqlalchemy_redshift import dialect + + # Your code here... + + - type: input + id: sqlalchemy_redshift_version + attributes: + label: sqlalchemy-redshift Version + description: What version are you using? + placeholder: "0.8.14" + + - type: input + id: sqlalchemy_version + attributes: + label: SQLAlchemy Version + description: What version of SQLAlchemy are you using? + placeholder: "2.0.43" \ No newline at end of file diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..8208190a --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,62 @@ +# Security Policy + +## Supported Versions + +We provide security updates for the following versions of sqlalchemy-redshift: + +| Version | Supported | +| ------- | ------------------ | +| 0.8.x | โœ… | +| < 0.8 | โŒ | + +## Reporting a Vulnerability + +We take security vulnerabilities seriously. If you discover a security vulnerability in sqlalchemy-redshift, please report it responsibly. + +### How to Report + +1. **DO NOT** create a public GitHub issue for security vulnerabilities +2. Email security details to the maintainers through GitHub's private vulnerability reporting feature +3. Or create a private security advisory through GitHub's interface + +### What to Include + +When reporting a vulnerability, please include: + +- A clear description of the vulnerability +- Steps to reproduce the issue +- Potential impact and affected versions +- Any suggested fixes or mitigation strategies +- Your contact information for follow-up questions + +### Response Timeline + +- **Acknowledgment**: We will acknowledge receipt within 2 business days +- **Initial Response**: We will provide an initial response within 5 business days +- **Fix Timeline**: We aim to release fixes for high-severity issues within 30 days + +### Disclosure Policy + +- We will work with you to understand and resolve the issue +- We will coordinate disclosure timing to ensure users have time to update +- We will credit you in our security advisory (unless you prefer to remain anonymous) + +### Security Best Practices + +When using sqlalchemy-redshift: + +1. **Keep Dependencies Updated**: Regularly update to the latest version +2. **Secure Connections**: Always use SSL/TLS for database connections +3. **Credential Management**: Never hardcode credentials in your code +4. **Input Validation**: Always validate and sanitize user inputs +5. **Least Privilege**: Use database users with minimal required permissions + +## Security Features + +sqlalchemy-redshift includes several security features: + +- SSL/TLS connection support by default +- Parameterized queries to prevent SQL injection +- Support for IAM role-based authentication with AWS Redshift + +Thank you for helping keep sqlalchemy-redshift secure! \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5843d596 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,39 @@ +version: 2 +updates: + # Enable version updates for Python dependencies + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 10 + reviewers: + - "sqlalchemy-redshift-maintainers" + assignees: + - "sqlalchemy-redshift-maintainers" + commit-message: + prefix: "deps" + include: "scope" + labels: + - "dependencies" + - "python" + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "10:00" + open-pull-requests-limit: 5 + reviewers: + - "sqlalchemy-redshift-maintainers" + assignees: + - "sqlalchemy-redshift-maintainers" + commit-message: + prefix: "ci" + include: "scope" + labels: + - "dependencies" + - "github-actions" \ No newline at end of file diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 00000000..20404f20 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,150 @@ +# Standard labels +- name: bug + color: d73a4a + description: Something isn't working + +- name: documentation + color: 0075ca + description: Improvements or additions to documentation + +- name: duplicate + color: cfd3d7 + description: This issue or pull request already exists + +- name: enhancement + color: a2eeef + description: New feature or request + +- name: good first issue + color: 7057ff + description: Good for newcomers + +- name: help wanted + color: 008672 + description: Extra attention is needed + +- name: invalid + color: e4e669 + description: This doesn't seem right + +- name: question + color: d876e3 + description: Further information is requested + +- name: wontfix + color: ffffff + description: This will not be worked on + +# Priority labels +- name: priority/low + color: 0e8a16 + description: Low priority + +- name: priority/medium + color: fbca04 + description: Medium priority + +- name: priority/high + color: d93f0b + description: High priority + +- name: priority/critical + color: b60205 + description: Critical priority + +# Type labels +- name: type/feature + color: a2eeef + description: New feature + +- name: type/bug + color: d73a4a + description: Bug fix + +- name: type/maintenance + color: fef2c0 + description: Maintenance work + +- name: type/security + color: d73a4a + description: Security related + +# Status labels +- name: status/needs-triage + color: f9d0c4 + description: Needs to be triaged + +- name: status/in-progress + color: c2e0c6 + description: Work in progress + +- name: status/blocked + color: b60205 + description: Blocked by something + +- name: status/waiting-for-feedback + color: fbca04 + description: Waiting for feedback + +# Component labels +- name: component/core + color: 5319e7 + description: Core dialect functionality + +- name: component/ddl + color: 5319e7 + description: DDL compilation (CREATE, ALTER, DROP) + +- name: component/copy + color: 5319e7 + description: COPY command functionality + +- name: component/reflection + color: 5319e7 + description: Table reflection/introspection + +- name: component/types + color: 5319e7 + description: Data types + +- name: component/tests + color: 5319e7 + description: Test suite + +# Dependencies +- name: dependencies + color: 0366d6 + description: Pull requests that update a dependency file + +- name: python + color: 3776ab + description: Python related + +- name: github-actions + color: 2088ff + description: GitHub Actions related + +# Special labels +- name: breaking-change + color: b60205 + description: Breaking change + +- name: backwards-compatible + color: 0e8a16 + description: Backwards compatible change + +- name: needs-docs + color: f9d0c4 + description: Needs documentation + +- name: needs-tests + color: f9d0c4 + description: Needs tests + +- name: stale + color: 888888 + description: Stale issue or PR + +- name: pinned + color: 000000 + description: Pinned issue or PR \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..2dec3b54 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,73 @@ +# Pull Request + +## Description + + +## Type of Change + +- [ ] ๐Ÿ› Bug fix (non-breaking change that fixes an issue) +- [ ] โœจ New feature (non-breaking change that adds functionality) +- [ ] ๐Ÿ’ฅ Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] ๐Ÿ“š Documentation update +- [ ] ๐Ÿ”ง Maintenance/refactoring +- [ ] ๐Ÿงช Test improvements +- [ ] โฌ†๏ธ Dependency updates + +## Related Issues + +Fixes # +Closes # +Related to # + +## Changes Made + +- +- +- + +## Testing + +- [ ] All existing tests pass +- [ ] Added new tests for new functionality +- [ ] Tested manually against real Redshift instance +- [ ] Updated documentation if needed + +### Test Results + +``` +Test results here... +``` + +## Checklist + +- [ ] I have read the [contributing guidelines](CONTRIBUTING.md) +- [ ] My code follows the project's coding standards +- [ ] I have performed a self-review of my code +- [ ] I have commented my code where necessary +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings or errors +- [ ] I have added tests that prove my fix is effective or my feature works +- [ ] New and existing unit tests pass locally +- [ ] Any dependent changes have been merged and published + +## Breaking Changes + +- None + +OR + +- **What breaks**: +- **Migration guide**: + +## Additional Notes + + +## Screenshots/Logs + + +--- + +**By submitting this PR, I confirm that:** +- I have the right to submit this code under the project's license +- I understand that this code will be reviewed before merging +- I agree to address any feedback provided during code review \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..dfecef45 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,53 @@ +name: "CodeQL Security Analysis" + +on: + push: + branches: [ "main", "develop" ] + pull_request: + branches: [ "main" ] + schedule: + # Run at 2:00 AM UTC every Monday + - cron: '0 2 * * 1' + +jobs: + analyze: + name: Analyze Code + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --dev + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml new file mode 100644 index 00000000..3023f3a1 --- /dev/null +++ b/.github/workflows/label-sync.yml @@ -0,0 +1,26 @@ +name: Sync Labels + +on: + push: + branches: [main] + paths: ['.github/labels.yml'] + workflow_dispatch: + +permissions: + issues: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Sync labels + uses: micnncim/action-label-syncer@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + manifest: .github/labels.yml + repository: ${{ github.repository }} + prune: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..83bbebc7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,158 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release' + required: true + type: string + +permissions: + contents: write + packages: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Install build dependencies + run: uv sync --dev + + - name: Build package + run: uv build + + - name: Check package + run: | + uv add --dev twine + uv run twine check dist/* + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 + + test: + runs-on: ubuntu-latest + needs: build + strategy: + matrix: + python-version: ['3.11', '3.12', '3.13'] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Install package + run: | + uv venv + source .venv/bin/activate + pip install dist/*.whl + + - name: Test installation + run: | + source .venv/bin/activate + python -c "import sqlalchemy_redshift; print(sqlalchemy_redshift.__version__)" + + publish: + runs-on: ubuntu-latest + needs: [build, test] + if: startsWith(github.ref, 'refs/tags/v') + environment: + name: release + url: https://pypi.org/project/sqlalchemy-redshift/ + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + print-hash: true + + github-release: + runs-on: ubuntu-latest + needs: [build, test] + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Generate changelog + id: changelog + run: | + # Get the latest tag + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + if [ -n "$LATEST_TAG" ]; then + PREVIOUS_TAG=$(git describe --tags --abbrev=0 "$LATEST_TAG"^ 2>/dev/null || echo "") + else + PREVIOUS_TAG="" + fi + + echo "## What's Changed" > CHANGELOG.md + echo "" >> CHANGELOG.md + + if [ -n "$PREVIOUS_TAG" ]; then + git log --pretty=format:"- %s (%h)" "$PREVIOUS_TAG..${{ github.ref_name }}" >> CHANGELOG.md + else + git log --pretty=format:"- %s (%h)" >> CHANGELOG.md + fi + + echo "" >> CHANGELOG.md + echo "" >> CHANGELOG.md + echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/$PREVIOUS_TAG...${{ github.ref_name }}" >> CHANGELOG.md + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + body_path: CHANGELOG.md + draft: false + prerelease: ${{ contains(github.ref_name, 'rc') || contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }} + generate_release_notes: true \ No newline at end of file diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..4e2a85e6 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,71 @@ +name: Security Checks + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + schedule: + # Run security checks every day at 3 AM UTC + - cron: '0 3 * * *' + +jobs: + security: + runs-on: ubuntu-latest + name: Security Analysis + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --dev + + - name: Run safety check for known vulnerabilities + run: | + uv add --dev safety + uv run safety check --json --output safety-report.json || true + + - name: Run bandit security linter + run: | + uv add --dev bandit[toml] + uv run bandit -r sqlalchemy_redshift/ -f json -o bandit-report.json || true + + - name: Upload safety report + if: always() + uses: actions/upload-artifact@v4 + with: + name: safety-report + path: safety-report.json + retention-days: 30 + + - name: Upload bandit report + if: always() + uses: actions/upload-artifact@v4 + with: + name: bandit-report + path: bandit-report.json + retention-days: 30 + + - name: Run pip-audit for known vulnerabilities + run: | + uv add --dev pip-audit + uv run pip-audit --format=json --output=pip-audit-report.json || true + + - name: Upload pip-audit report + if: always() + uses: actions/upload-artifact@v4 + with: + name: pip-audit-report + path: pip-audit-report.json + retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..ef44726e --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,56 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '0 0 * * *' # Run daily at midnight + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + # Issues + days-before-issue-stale: 60 + days-before-issue-close: 14 + stale-issue-label: 'stale' + stale-issue-message: | + This issue has been automatically marked as stale because it has not had recent activity. + It will be closed if no further activity occurs within 14 days. + + If this issue is still relevant, please comment to keep it open. + Thank you for your contributions! + close-issue-message: | + This issue has been automatically closed due to inactivity. + + If you believe this issue is still relevant, please reopen it or create a new issue + with updated information. + + # Pull requests + days-before-pr-stale: 30 + days-before-pr-close: 7 + stale-pr-label: 'stale' + stale-pr-message: | + This pull request has been automatically marked as stale because it has not had recent activity. + It will be closed if no further activity occurs within 7 days. + + If this PR is still relevant, please comment or push new commits to keep it open. + Thank you for your contributions! + close-pr-message: | + This pull request has been automatically closed due to inactivity. + + If you believe this PR is still relevant, please reopen it or create a new PR + with updated changes. + + # Exemptions + exempt-issue-labels: 'pinned,security,blocked' + exempt-pr-labels: 'pinned,security,blocked,work-in-progress' + exempt-all-assignees: true + exempt-draft-pr: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index efcc2a3d..aeee1171 100644 --- a/.gitignore +++ b/.gitignore @@ -36,10 +36,10 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ -.tox/ .coverage .coverage.* .cache +.pytest_cache/ nosetests.xml coverage.xml *,cover @@ -59,3 +59,7 @@ target/ # IDE .idea/ + +# uv +.python-version +uv.lock diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 8847d72b..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -include CHANGES.rst -include README.rst - -include sqlalchemy_redshift/redshift-ca-bundle.crt - -recursive-exclude * __pycache__ -recursive-exclude * *.py[co] diff --git a/README.rst b/README.rst index a0933969..b2aaa97d 100644 --- a/README.rst +++ b/README.rst @@ -44,11 +44,7 @@ To install dependencies and run tests:: $ uv sync --dev $ uv run pytest tests/ -Alternatively, you can still use tox:: - - $ tox - -However, integration tests will not run unless the following +Integration tests will not run unless the following environment variables are set: * REDSHIFT_HOST diff --git a/buildspec_test.yml b/buildspec_test.yml new file mode 100644 index 00000000..5038547f --- /dev/null +++ b/buildspec_test.yml @@ -0,0 +1,52 @@ +version: 0.2 + +phases: + install: + commands: + # start install test dependencies (specific for Python 3.13) + - yum -y install bzip2-devel libffi-devel + - yum -y groupinstall "Development Tools" + - orig_dir=$PWD + - cd /opt + - curl https://ftp.openssl.org/source/old/1.1.1/openssl-1.1.1j.tar.gz --output openssl.tar.gz + - tar xzf openssl.tar.gz + - rm openssl.tar.gz + - cd openssl-1.1.1j/ + - ./config --prefix=/opt/openssl && make && make install + # end install test dependencies + # start install Python + - version=3.13.1 + - wget https://www.python.org/ftp/python/$version/Python-$version.tar.xz + - tar xJf Python-$version.tar.xz + - cd Python-$version + - ./configure --enable-optimizations --with-openssl=/opt/openssl + - make + - make altinstall + # end install Python + - cd $orig_dir + pre_build: + commands: + - pip3.13 install uv + - uv sync --dev + build: + commands: + # Wait for Redshift Serverless workgroup to be available + - echo "Waiting for Redshift Serverless workgroup to be available..." + - | + max_attempts=30 + attempt=0 + while [ $attempt -lt $max_attempts ]; do + if aws redshift-serverless get-workgroup --workgroup-name sqlalchemy-redshift-test --region $AWS_DEFAULT_REGION 2>/dev/null | grep -q "AVAILABLE"; then + echo "Redshift Serverless workgroup is available" + break + fi + echo "Attempt $((attempt+1))/$max_attempts: Workgroup not ready yet, waiting 30 seconds..." + sleep 30 + attempt=$((attempt+1)) + done + if [ $attempt -eq $max_attempts ]; then + echo "Timeout waiting for Redshift Serverless workgroup" + exit 1 + fi + # Run tests + - uv run pytest tests/ --dbdriver psycopg2 --dbdriver psycopg2cffi --dbdriver redshift_connector $TEST_COMMAND \ No newline at end of file diff --git a/ci/build-spec.yml b/ci/build-spec.yml index ba8927d9..a849dcad 100644 --- a/ci/build-spec.yml +++ b/ci/build-spec.yml @@ -3,7 +3,7 @@ version: 0.2 phases: install: commands: - # start install test dependencies (specific for Python 3.10) + # start install test dependencies (specific for Python 3.13) - yum -y install bzip2-devel libffi-devel - yum -y groupinstall "Development Tools" - orig_dir=$PWD @@ -15,7 +15,7 @@ phases: - ./config --prefix=/opt/openssl && make && make install # end install test dependencies # start install Python - - version=3.10.10 + - version=3.13.1 - wget https://www.python.org/ftp/python/$version/Python-$version.tar.xz - tar xJf Python-$version.tar.xz - cd Python-$version diff --git a/ci/ci-pipeline.yml b/ci/ci-pipeline.yml index 205f566e..2208e0f8 100644 --- a/ci/ci-pipeline.yml +++ b/ci/ci-pipeline.yml @@ -101,19 +101,21 @@ Resources: Properties: RouteTableId: !Ref DemoVPCPrivateRouteTable SubnetId: !Ref DemoVPCPrivateSubnetB - DemoVPCRSSubnetGroup: - Type: AWS::Redshift::ClusterSubnetGroup + RedshiftServerlessNamespace: + Type: AWS::RedshiftServerless::Namespace Properties: - Description: "Subnet group for Redshift Devops Demo VPC." - SubnetIds: - - !Ref DemoVPCPrivateSubnetA - - !Ref DemoVPCPrivateSubnetB - TestRedshiftClusterSecrets: + NamespaceName: sqlalchemy-redshift-test + AdminUsername: !Ref TestRedshiftUsername + AdminUserPassword: !Ref TestRedshiftPassword + DbName: dev + IamRoles: + - !GetAtt RedshiftIngestIAMRole.Arn + TestRedshiftServerlessSecrets: Type: AWS::SecretsManager::Secret Properties: SecretString: !Sub - - '{"username": "${username}", "password": "${password}", "host": "${host}", "port": ${port}, "dbName": "${dbName}"}' - - {username: !Ref TestRedshiftUsername, password: !Ref TestRedshiftPassword, host: !GetAtt TestRedshiftCluster.Endpoint.Address, port: !GetAtt TestRedshiftCluster.Endpoint.Port, dbName: "dev"} + - '{"username": "${username}", "password": "${password}", "host": "${host}", "port": 5439, "dbName": "${dbName}"}' + - {username: !Ref TestRedshiftUsername, password: !Ref TestRedshiftPassword, host: !GetAtt RedshiftServerlessWorkgroup.Workgroup.Endpoint.Address, dbName: "dev"} CodePipelineIAMRole: Type: AWS::IAM::Role Properties: @@ -142,24 +144,22 @@ Resources: - Action: ["codecommit:GetRepository", "codecommit:CancelUploadArchive", "codecommit:GetBranch", "codecommit:GetCommit", "codecommit:GetUploadArchiveStatus", "codecommit:UploadArchive"] Resource: "*" Effect: Allow - TestRedshiftCluster: - Type: AWS::Redshift::Cluster + RedshiftServerlessWorkgroup: + Type: AWS::RedshiftServerless::Workgroup Properties: - VpcSecurityGroupIds: - - !Ref RSClusterSecurityGroup - ClusterSubnetGroupName: !Ref DemoVPCRSSubnetGroup - ClusterType: "single-node" - NodeType: "dc2.large" - IamRoles: - - !GetAtt RedshiftIngestIAMRole.Arn - MasterUsername: !Ref TestRedshiftUsername - MasterUserPassword: !Ref TestRedshiftPassword - DBName: "dev" - Encrypted: true - RSClusterSecurityGroup: + WorkgroupName: sqlalchemy-redshift-test + NamespaceName: !Ref RedshiftServerlessNamespace + BaseCapacity: 8 + SecurityGroupIds: + - !Ref RSServerlessSecurityGroup + SubnetIds: + - !Ref DemoVPCPrivateSubnetA + - !Ref DemoVPCPrivateSubnetB + PubliclyAccessible: false + RSServerlessSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: - GroupDescription: "Security group for Redshift clusters" + GroupDescription: "Security group for Redshift Serverless" SecurityGroupIngress: - IpProtocol: tcp FromPort: 5439 @@ -240,7 +240,17 @@ Resources: Action: - secretsmanager:GetSecretValue Resource: - - !Ref TestRedshiftClusterSecrets + - !Ref TestRedshiftServerlessSecrets + - Effect: Allow + Action: + - redshift-serverless:GetWorkgroup + - redshift-serverless:GetNamespace + - redshift-serverless:CreateSnapshot + - redshift-serverless:DeleteSnapshot + - redshift-serverless:ListSnapshots + - redshift-serverless:ListWorkgroups + - redshift-serverless:ListNamespaces + Resource: "*" - Effect: Allow Action: - ec2:CreateNetworkInterfacePermission @@ -283,27 +293,27 @@ Resources: - Name: REDSHIFT_HOST Value: !Sub - '${arn}:host' - - {arn: !Ref TestRedshiftClusterSecrets} + - {arn: !Ref TestRedshiftServerlessSecrets} Type: SECRETS_MANAGER - Name: REDSHIFT_USERNAME Value: !Sub - '${arn}:username' - - {arn: !Ref TestRedshiftClusterSecrets} + - {arn: !Ref TestRedshiftServerlessSecrets} Type: SECRETS_MANAGER - Name: PGPASSWORD Value: !Sub - '${arn}:password' - - {arn: !Ref TestRedshiftClusterSecrets} + - {arn: !Ref TestRedshiftServerlessSecrets} Type: SECRETS_MANAGER - Name: REDSHIFT_PORT Value: !Sub - '${arn}:port' - - {arn: !Ref TestRedshiftClusterSecrets} + - {arn: !Ref TestRedshiftServerlessSecrets} Type: SECRETS_MANAGER - Name: REDSHIFT_DATABASE Value: !Sub - '${arn}:dbName' - - {arn: !Ref TestRedshiftClusterSecrets} + - {arn: !Ref TestRedshiftServerlessSecrets} Type: SECRETS_MANAGER - Name: REDSHIFT_IAM_ROLE_NAME Value: !GetAtt RedshiftIngestIAMRole.Arn diff --git a/pyproject.toml b/pyproject.toml index 6a1c719f..26fe6af0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ authors = [ maintainers = [ {name = "Thomas Grainger", email = "sqlalchemy-redshift@graingert.co.uk"}, ] -requires-python = ">=3.4" +requires-python = ">=3.11" classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", @@ -23,17 +23,13 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ - "SQLAlchemy>=0.9.2,<2.0.0", - "packaging", + "SQLAlchemy>=2.0.43", + "packaging>=20.0", ] [project.urls] @@ -54,29 +50,29 @@ sqlalchemy_redshift = ["redshift-ca-bundle.crt"] [project.optional-dependencies] dev = [ - "alembic==1.9.2", - "packaging==20.4", - "psycopg2==2.8.6", - "psycopg2cffi==2.8.1", - "pytest==7.2.1", - "requests==2.25.0", - "redshift_connector==2.0.907", - "flake8==4.0.1", + "alembic>=1.13.0", + "packaging>=20.0", + "psycopg2>=2.9.5", + "psycopg2cffi>=2.9.0", + "pytest>=8.0.0", + "requests>=2.31.0", + "redshift_connector>=2.1.0", + "flake8>=7.0.0", ] test = [ - "alembic==1.9.2", - "packaging==20.4", - "psycopg2==2.8.6", - "psycopg2cffi==2.8.1", - "pytest==7.2.1", - "requests==2.25.0", - "redshift_connector==2.0.907", + "alembic>=1.13.0", + "packaging>=20.0", + "psycopg2>=2.9.5", + "psycopg2cffi>=2.9.0", + "pytest>=8.0.0", + "requests>=2.31.0", + "redshift_connector>=2.1.0", ] docs = [ - "sphinx==1.6.3", - "numpydoc==0.6.0", - "psycopg2-binary==2.9.1", - "jinja2<3.1.0", + "sphinx>=7.0.0", + "numpydoc>=1.6.0", + "psycopg2-binary>=2.9.5", + "jinja2>=3.1.0", ] [tool.pytest.ini_options] @@ -85,12 +81,12 @@ doctest_optionflags = "NORMALIZE_WHITESPACE" [tool.uv] dev-dependencies = [ - "alembic==1.9.2", - "packaging==20.4", - "psycopg2==2.8.6", - "psycopg2cffi==2.8.1", - "pytest==7.2.1", - "requests==2.25.0", - "redshift_connector==2.0.907", - "flake8==4.0.1", + "alembic>=1.13.0", + "packaging>=20.0", + "psycopg2>=2.9.5", + "psycopg2cffi>=2.9.0", + "pytest>=8.0.0", + "requests>=2.31.0", + "redshift_connector>=2.1.0", + "flake8>=7.0.0", ] \ No newline at end of file diff --git a/redshift_sqlalchemy/__init__.py b/redshift_sqlalchemy/__init__.py deleted file mode 100644 index f86c9061..00000000 --- a/redshift_sqlalchemy/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Compatibility module for projects referencing sqlalchemy_redshift -by its old name "redshift_sqlalchemy". -""" - -import sys -import warnings - -import sqlalchemy_redshift - - -DEPRECATION_MESSAGE = """\ -redshift_sqlalchemy has been renamed to sqlalchemy_redshift. - -The redshift_sqlalchemy compatibility package will be removed in -a future release, so it is recommended to update all package references. -""" - -warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning) - -# All references to module redshift_sqlalchemy will map to sqlalchemy_redshift -sys.modules['redshift_sqlalchemy'] = sqlalchemy_redshift diff --git a/requirements-docs.txt b/requirements-docs.txt deleted file mode 100644 index da0a3e3f..00000000 --- a/requirements-docs.txt +++ /dev/null @@ -1,5 +0,0 @@ --e . -sphinx==1.6.3 -numpydoc==0.6.0 -psycopg2-binary==2.9.1 -jinja2<3.1.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79cf..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/setup.py b/setup.py deleted file mode 100644 index f6bd7181..00000000 --- a/setup.py +++ /dev/null @@ -1,51 +0,0 @@ -from setuptools import setup - -readme = open('README.rst').read() -history = open('CHANGES.rst').read().replace('.. :changelog:', '') - -setup( - name='sqlalchemy-redshift', - version='0.8.15.dev0', - description='Amazon Redshift Dialect for sqlalchemy', - long_description=readme + '\n\n' + history, - long_description_content_type='text/x-rst', - author='Matt George', - author_email='mgeorge@gmail.com', - maintainer='Thomas Grainger', - maintainer_email='sqlalchemy-redshift@graingert.co.uk', - license="MIT", - url='https://github.com/sqlalchemy-redshift/sqlalchemy-redshift', - packages=['sqlalchemy_redshift', 'redshift_sqlalchemy'], - package_data={'sqlalchemy_redshift': ['redshift-ca-bundle.crt']}, - python_requires='>=3.4', - install_requires=[ - # requires sqlalchemy.sql.base.DialectKWArgs.dialect_options, new in - # version 0.9.2 - 'SQLAlchemy>=0.9.2,<2.0.0', - 'packaging', - ], - classifiers=[ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - ], - entry_points={ - 'sqlalchemy.dialects': [ - 'redshift = sqlalchemy_redshift.dialect:RedshiftDialect_psycopg2', - 'redshift.psycopg2 = sqlalchemy_redshift.dialect:RedshiftDialect_psycopg2', - 'redshift.psycopg2cffi = sqlalchemy_redshift.dialect:RedshiftDialect_psycopg2cffi', - 'redshift.redshift_connector = sqlalchemy_redshift.dialect:RedshiftDialect_redshift_connector', - ] - }, -) diff --git a/sqlalchemy_redshift/__init__.py b/sqlalchemy_redshift/__init__.py index 934002cb..06395ee8 100644 --- a/sqlalchemy_redshift/__init__.py +++ b/sqlalchemy_redshift/__init__.py @@ -1,14 +1,19 @@ -from pkg_resources import DistributionNotFound, get_distribution, parse_version +import importlib.metadata as metadata +from packaging.version import Version for package in ['psycopg2', 'psycopg2-binary', 'psycopg2cffi']: try: - if get_distribution(package).parsed_version < parse_version('2.5'): + version = metadata.version(package) + if Version(version) < Version('2.5'): raise ImportError('Minimum required version for psycopg2 is 2.5') break - except DistributionNotFound: + except metadata.PackageNotFoundError: pass -__version__ = get_distribution('sqlalchemy-redshift').version +try: + __version__ = metadata.version('sqlalchemy-redshift') +except metadata.PackageNotFoundError: + __version__ = '0.8.15.dev0' # fallback for development from sqlalchemy.dialects import registry # noqa diff --git a/sqlalchemy_redshift/ddl.py b/sqlalchemy_redshift/ddl.py index 4f0cd659..b7755b6e 100644 --- a/sqlalchemy_redshift/ddl.py +++ b/sqlalchemy_redshift/ddl.py @@ -116,7 +116,7 @@ class CreateMaterializedView(DDLElement): ... sa.Column('id', sa.Integer, primary_key=True), ... sa.Column('name', sa.String) ... ) - >>> selectable = sa.select([user.c.id, user.c.name], from_obj=user) + >>> selectable = sa.select(user.c.id, user.c.name).select_from(user) >>> view = CreateMaterializedView( ... 'materialized_view_of_users', ... selectable, diff --git a/sqlalchemy_redshift/dialect.py b/sqlalchemy_redshift/dialect.py index ce755ea6..a94a2f7b 100644 --- a/sqlalchemy_redshift/dialect.py +++ b/sqlalchemy_redshift/dialect.py @@ -3,8 +3,9 @@ import re from collections import defaultdict, namedtuple from logging import getLogger +from pathlib import Path -import pkg_resources +# Removed pkg_resources usage - see __init__.py for version handling import sqlalchemy as sa from packaging.version import Version from sqlalchemy import inspect @@ -980,30 +981,80 @@ def _get_table_or_view_names(self, relkind, connection, schema=None, **kw): relation_names.append(key.name) return relation_names - def _get_column_info(self, *args, **kwargs): - kw = kwargs.copy() - encode = kw.pop('encode', None) - if sa_version >= Version('1.3.16'): - # SQLAlchemy 1.3.16 introduced generated columns, - # not supported in redshift - kw['generated'] = '' - - if sa_version < Version('1.4.0') and 'identity' in kw: - del kw['identity'] - elif sa_version >= Version('1.4.0') and 'identity' not in kw: - kw['identity'] = None - - column_info = super(RedshiftDialectMixin, self)._get_column_info( - *args, - **kw - ) - if isinstance(column_info['type'], VARCHAR): - if column_info['type'].length is None: - column_info['type'] = NullType() - if 'info' not in column_info: - column_info['info'] = {} + def _get_column_info(self, name, format_type, default, notnull, domains, enums, schema=None, encode=None, comment=None, identity=None, **kwargs): + """ + Custom column info processing for Redshift. + + In SQLAlchemy 2.0, the parent _get_column_info method was removed, + so we implement the logic directly. + """ + # Process the PostgreSQL format_type to get the base type + attype = format_type + nullable = not notnull + + # Handle array types + if attype.endswith('[]'): + attype = attype[:-2] + + # Map the format_type to a SQLAlchemy type + try: + coltype = self.ischema_names.get(attype) + if coltype is None: + # Fallback for unknown types + coltype = VARCHAR # Use VARCHAR as default + except (KeyError, AttributeError): + coltype = VARCHAR # fallback for unknown types + + # Handle length specification for types like varchar(30) + type_args = [] + + if '(' in format_type and format_type.endswith(')'): + # Extract length/precision from format_type like varchar(30) or numeric(10,2) + args_part = format_type[format_type.index('(')+1:-1] + if args_part: + try: + if ',' in args_part: + # For types like numeric(10,2) + type_args = [int(x.strip()) for x in args_part.split(',')] + else: + # For types like varchar(30) + type_args = [int(args_part)] + except ValueError: + pass # ignore parse errors + + # Create the column type instance + if type_args and isinstance(coltype, type): + coltype = coltype(*type_args) + elif isinstance(coltype, type): + coltype = coltype() + + # Redshift-specific handling: VARCHAR with no length becomes NullType + if isinstance(coltype, VARCHAR): + if not hasattr(coltype, 'length') or coltype.length is None: + coltype = NullType() + + # Build column info dict + column_info = { + 'name': name, + 'type': coltype, + 'nullable': nullable, + 'default': default, + } + + # Add identity info for SQLAlchemy 2.0+ + if identity is not None: + column_info['identity'] = identity + + # Add comment if present + if comment is not None: + column_info['comment'] = comment + + # Handle encode parameter (Redshift-specific) if encode and encode != 'none': + if 'info' not in column_info: + column_info['info'] = {} column_info['info']['encode'] = encode + return column_info def _get_redshift_relation(self, connection, table_name, @@ -1212,10 +1263,7 @@ def create_connect_args(self, *args, **kwargs): """ default_args = { 'sslmode': 'verify-full', - 'sslrootcert': pkg_resources.resource_filename( - __name__, - 'redshift-ca-bundle.crt' - ), + 'sslrootcert': str(Path(__file__).parent / 'redshift-ca-bundle.crt'), } cargs, cparams = ( super(Psycopg2RedshiftDialectMixin, self).create_connect_args( @@ -1226,7 +1274,7 @@ def create_connect_args(self, *args, **kwargs): return cargs, default_args @classmethod - def dbapi(cls): + def import_dbapi(cls): try: return importlib.import_module(cls.driver) except ImportError: @@ -1234,11 +1282,20 @@ def dbapi(cls): 'No module named {}'.format(cls.driver) ) + @classmethod + def dbapi(cls): + # Backward compatibility - calls import_dbapi + return cls.import_dbapi() + class RedshiftDialect_psycopg2( Psycopg2RedshiftDialectMixin, PGDialect_psycopg2 ): supports_statement_cache = False + + @classmethod + def import_dbapi(cls): + return super().import_dbapi() # Add RedshiftDialect synonym for backwards compatibility. @@ -1309,7 +1366,7 @@ def __init__(self, client_encoding=None, **kwargs): self.client_encoding = client_encoding @classmethod - def dbapi(cls): + def import_dbapi(cls): try: driver_module = importlib.import_module(cls.driver) @@ -1322,10 +1379,14 @@ def dbapi(cls): return driver_module except ImportError: raise ImportError( - 'No module named redshift_connector. Please install ' - 'redshift_connector to use this sqlalchemy dialect.' + 'No module named {}'.format(cls.driver) ) + @classmethod + def dbapi(cls): + # Backward compatibility - calls import_dbapi + return cls.import_dbapi() + def set_client_encoding(self, connection, client_encoding): """ Sets the client-side encoding using the provided connection object. diff --git a/tests/rs_sqla_test_utils/models.py b/tests/rs_sqla_test_utils/models.py index c594fa0a..f1c6d356 100644 --- a/tests/rs_sqla_test_utils/models.py +++ b/tests/rs_sqla_test_utils/models.py @@ -2,10 +2,10 @@ from sqlalchemy import event from sqlalchemy import DDL -from sqlalchemy.ext import declarative +from sqlalchemy.orm import declarative_base -Base = declarative.declarative_base() +Base = declarative_base() event.listen( Base.metadata, 'before_create', diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 9f02f005..c811cae4 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -3,6 +3,6 @@ def test_func_now(stub_redshift_dialect): dialect = stub_redshift_dialect - s = select([func.NOW().label("time")]) + s = select(func.NOW().label("time")) compiled = s.compile(dialect=dialect) assert str(compiled) == "SELECT SYSDATE AS time" diff --git a/tests/test_copy_command.py b/tests/test_copy_command.py index 8783978d..28a35671 100644 --- a/tests/test_copy_command.py +++ b/tests/test_copy_command.py @@ -3,6 +3,7 @@ from sqlalchemy import exc as sa_exc from sqlalchemy_redshift import dialect +from sqlalchemy_redshift.commands import Format, Compression from rs_sqla_test_utils.utils import clean, compile_query access_key_id = 'IO1IWSZL5YRFM3BEW256' @@ -174,7 +175,7 @@ def test_format(stub_redshift_dialect): data_location='s3://mybucket/data/listing/', access_key_id=access_key_id, secret_access_key=secret_access_key, - format='JSON', + format=Format.json, truncate_columns=True, delimiter=',', ignore_header=0, @@ -246,7 +247,7 @@ def test_compression(stub_redshift_dialect): data_location='s3://mybucket/data/listing/', access_key_id=access_key_id, secret_access_key=secret_access_key, - compression='LZOP', + compression=Compression.lzop, truncate_columns=True, delimiter=',', ignore_header=0, @@ -285,7 +286,7 @@ def test_ascii_nul_as_redshift_null(stub_redshift_dialect): data_location='s3://mybucket/data/listing/', access_key_id=access_key_id, secret_access_key=secret_access_key, - compression='BZIP2', + compression=Compression.bzip2, dangerous_null_delimiter=u'\000', truncate_columns=True, delimiter=',', @@ -313,9 +314,9 @@ def test_json_upload_with_manifest_ordered_columns(stub_redshift_dialect): access_key_id=access_key_id, secret_access_key=secret_access_key, manifest=True, - format='JSON', + format=Format.json, path_file='s3://mybucket/data/jsonpath.json', - compression='GZIP', + compression=Compression.gzip, time_format='auto', accept_any_date=True, ) diff --git a/tests/test_default_ssl.py b/tests/test_default_ssl.py index f5586d30..a950c864 100644 --- a/tests/test_default_ssl.py +++ b/tests/test_default_ssl.py @@ -1,11 +1,11 @@ import sqlalchemy as sa -from pkg_resources import resource_filename +from pathlib import Path from sqlalchemy_redshift.dialect import ( Psycopg2RedshiftDialectMixin, RedshiftDialect_redshift_connector ) -CERT_PATH = resource_filename("sqlalchemy_redshift", "redshift-ca-bundle.crt") +CERT_PATH = str(Path(__file__).parent.parent / "sqlalchemy_redshift" / "redshift-ca-bundle.crt") def test_ssl_args(redshift_dialect_flavor): diff --git a/tests/test_delete_stmt.py b/tests/test_delete_stmt.py index 62089c04..f0d53949 100644 --- a/tests/test_delete_stmt.py +++ b/tests/test_delete_stmt.py @@ -174,9 +174,7 @@ def test_delete_stmt_subqueryplusjoin(stub_redshift_dialect): orders ).where( orders.c.customer_id.in_( - sa.select( - [customers.c.id] - ).where(customers.c.email.endswith('test.com')) + sa.select(customers.c.id).where(customers.c.email.endswith('test.com')) ) ).where( orders.c.id == items.c.order_id @@ -201,9 +199,7 @@ def test_delete_stmt_subquery(stub_redshift_dialect): orders ).where( orders.c.customer_id.in_( - sa.select( - [customers.c.id] - ).where(customers.c.email.endswith('test.com')) + sa.select(customers.c.id).where(customers.c.email.endswith('test.com')) ) ) expected = """ @@ -221,9 +217,7 @@ def test_delete_stmt_on_subquerycomma(stub_redshift_dialect): ham ).where( ham.c.id.in_( - sa.select( - [hammy_spam.c.ham_id] - ) + sa.select(hammy_spam.c.ham_id) ) ) expected = """ @@ -266,7 +260,7 @@ def test_delete_stmt_with_comma_subquery_alias_join(stub_redshift_dialect): items.c.order_id == orders.c.id ).where( orders.c.customer_id.in_( - sa.select([customers.c.id]).where( + sa.select(customers.c.id).where( customers.c.email.endswith('test.com') ) ) diff --git a/tests/test_dialects.py b/tests/test_dialects.py index 0b8e522a..9ba796bf 100644 --- a/tests/test_dialects.py +++ b/tests/test_dialects.py @@ -5,7 +5,7 @@ ) from sqlalchemy.dialects.postgresql.base import PGDialect -from redshift_sqlalchemy import dialect +from sqlalchemy_redshift import dialect from rs_sqla_test_utils.utils import make_mock_engine diff --git a/tests/test_materialized_views.py b/tests/test_materialized_views.py index b8558d67..c2882219 100644 --- a/tests/test_materialized_views.py +++ b/tests/test_materialized_views.py @@ -11,7 +11,7 @@ def selectable(): MetaData(), Column('id', Integer, primary_key=True), Column('name', String)) - return select([table.c.id, table.c.name], from_obj=table) + return select(table.c.id, table.c.name).select_from(table) def test_basic_materialized_view(selectable, stub_redshift_dialect): @@ -63,7 +63,7 @@ def test_distkey_materialized_view(selectable, stub_redshift_dialect): DISTKEY (id) AS SELECT t1.id, t1.name FROM t1 """ - for key in ("id", selectable.c.id): + for key in ("id", selectable.selected_columns.id): view = dialect.CreateMaterializedView( "test_view", selectable, @@ -79,7 +79,7 @@ def test_sortkey_materialized_view(selectable, stub_redshift_dialect): SORTKEY (id) AS SELECT t1.id, t1.name FROM t1 """ - for key in ("id", selectable.c.id): + for key in ("id", selectable.selected_columns.id): view = dialect.CreateMaterializedView( "test_view", selectable, @@ -97,7 +97,7 @@ def test_interleaved_sortkey_materialized_view( INTERLEAVED SORTKEY (id) AS SELECT t1.id, t1.name FROM t1 """ - for key in ("id", selectable.c.id): + for key in ("id", selectable.selected_columns.id): view = dialect.CreateMaterializedView( "test_view", selectable, diff --git a/tests/test_unload_from_select.py b/tests/test_unload_from_select.py index 48cee5d5..c8373921 100644 --- a/tests/test_unload_from_select.py +++ b/tests/test_unload_from_select.py @@ -30,7 +30,7 @@ def test_basic_unload_case(stub_redshift_dialect): """Tests that the simplest type of UnloadFromSelect works.""" unload = dialect.UnloadFromSelect( - select=sa.select([sa.func.count(table.c.id)]), + select=sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', access_key_id=access_key_id, secret_access_key=secret_access_key, @@ -57,7 +57,7 @@ def test_iam_role( creds = f'aws_iam_role={iam_role_arn}' unload = dialect.UnloadFromSelect( - select=sa.select([sa.func.count(table.c.id)]), + select=sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', aws_account_id=aws_account_id, iam_role_name=iam_role_name, @@ -81,7 +81,7 @@ def test_iam_role_partition( creds = f'aws_iam_role={iam_role_arn_with_aws_partition}' unload = dialect.UnloadFromSelect( - select=sa.select([sa.func.count(table.c.id)]), + select=sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', aws_partition='aws-us-gov', aws_account_id='000123456789', @@ -107,7 +107,7 @@ def test_iam_role_partition_validation(): with pytest.raises(ValueError): dialect.UnloadFromSelect( - select=sa.select([sa.func.count(table.c.id)]), + select=sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', aws_partition=aws_partition, aws_account_id=aws_account_id, @@ -121,7 +121,7 @@ def test_iam_role_arns_list(stub_redshift_dialect, iam_role_arns): creds = f'aws_iam_role={",".join(iam_role_arns)}' unload = dialect.UnloadFromSelect( - select=sa.select([sa.func.count(table.c.id)]), + select=sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', iam_role_arns=iam_role_arns, ) @@ -142,7 +142,7 @@ def test_iam_role_arns_single(stub_redshift_dialect, iam_role_arn): creds = f'aws_iam_role={iam_role_arn}' unload = dialect.UnloadFromSelect( - select=sa.select([sa.func.count(table.c.id)]), + select=sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', iam_role_arns=iam_role_arn, ) @@ -161,7 +161,7 @@ def test_all_redshift_options(stub_redshift_dialect): """Tests that UnloadFromSelect handles all options correctly.""" unload = dialect.UnloadFromSelect( - sa.select([sa.func.count(table.c.id)]), + sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', access_key_id=access_key_id, secret_access_key=secret_access_key, @@ -205,7 +205,7 @@ def test_all_redshift_options_with_header(stub_redshift_dialect): """Tests that UnloadFromSelect handles all options correctly.""" unload = dialect.UnloadFromSelect( - sa.select([sa.func.count(table.c.id)]), + sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', access_key_id=access_key_id, secret_access_key=secret_access_key, @@ -249,7 +249,7 @@ def test_csv_format__basic(stub_redshift_dialect): """Tests that UnloadFromSelect uses the format option correctly.""" unload = dialect.UnloadFromSelect( - select=sa.select([sa.func.count(table.c.id)]), + select=sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', access_key_id=access_key_id, secret_access_key=secret_access_key, @@ -279,7 +279,7 @@ def test_csv_format__bad_options_crash( FIXEDWIDTH with the CSV format. """ unload = dialect.UnloadFromSelect( - select=sa.select([sa.func.count(table.c.id)]), + select=sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', access_key_id=access_key_id, secret_access_key=secret_access_key, @@ -295,7 +295,7 @@ def test_csv_format__bad_options_crash( def test_parquet_format__basic(stub_redshift_dialect): """Basic successful test of unloading with the Parquet format.""" unload = dialect.UnloadFromSelect( - select=sa.select([sa.func.count(table.c.id)]), + select=sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', access_key_id=access_key_id, secret_access_key=secret_access_key, @@ -324,7 +324,7 @@ def test_parquet_format__basic(stub_redshift_dialect): def test_parquet_format__bad_options_crash(kwargs, stub_redshift_dialect): """Verify we crash if we use the Parquet format with a bad option.""" unload = dialect.UnloadFromSelect( - select=sa.select([sa.func.count(table.c.id)]), + select=sa.select(sa.func.count(table.c.id)), unload_location='s3://bucket/key', access_key_id=access_key_id, secret_access_key=secret_access_key, diff --git a/tox.ini b/tox.ini deleted file mode 100644 index b24a463b..00000000 --- a/tox.ini +++ /dev/null @@ -1,42 +0,0 @@ -[tox] -envlist = - py39-pg28-sa13 - py39-pg28-sa14 - py310-pg28-sa13 - py310-pg28-sa14 - lint - docs - -[testenv] -commands = pytest {posargs} --dbdriver psycopg2 --dbdriver psycopg2cffi --dbdriver redshift_connector -passenv = PGPASSWORD,REDSHIFT_USERNAME,REDSHIFT_HOST,REDSHIFT_PORT,REDSHIFT_DATABASE,REDSHIFT_IAM_ROLE_ARN,AWS_ACCOUNT_ID,REDSHIFT_IAM_ROLE_NAME -deps = - sa13: sqlalchemy==1.3.24 - sa14: sqlalchemy==1.4.15 - pg28: psycopg2==2.8.6 - pg29: psycopg2==2.9.5 - alembic==1.9.2 - packaging==20.4 - psycopg2cffi==2.8.1 - pytest==7.2.1 - requests==2.25.0 - redshift_connector==2.0.907 - requests==2.25.0 - -[testenv:lint] -deps = - flake8==4.0.1 - psycopg2 - redshift_connector -commands=flake8 sqlalchemy_redshift tests - -[testenv:docs] -changedir=docs -deps= - -rrequirements-docs.txt -commands= - sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html - -[pytest] -addopts = --doctest-modules --doctest-glob='*.rst' --ignore=setup.py --ignore=docs/conf.py -doctest_optionflags = NORMALIZE_WHITESPACE From 029a5b4c1a3ca3ed4e034b210cb61500c81b11c6 Mon Sep 17 00:00:00 2001 From: Jagat Singh Date: Sat, 6 Sep 2025 03:18:21 +0530 Subject: [PATCH 3/5] Add version note --- README.rst | 2 ++ sqlalchemy_redshift/dialect.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index b2aaa97d..f45f48b5 100644 --- a/README.rst +++ b/README.rst @@ -3,6 +3,8 @@ sqlalchemy-redshift Amazon Redshift dialect for SQLAlchemy. +NOTE: This has been updated for SQLAlchemy 2.X and uses latest Python version. See the local release in this Git repo. + Installation ------------ diff --git a/sqlalchemy_redshift/dialect.py b/sqlalchemy_redshift/dialect.py index a94a2f7b..cee381f7 100644 --- a/sqlalchemy_redshift/dialect.py +++ b/sqlalchemy_redshift/dialect.py @@ -1429,9 +1429,8 @@ def on_connect(self): fns = [] def on_connect(conn): - from sqlalchemy import util from sqlalchemy.sql.elements import quoted_name - conn.py_types[quoted_name] = conn.py_types[util.text_type] + conn.py_types[quoted_name] = conn.py_types[str] fns.append(on_connect) From 8ba44d9e854df67361bb8248015b5597ac925b9c Mon Sep 17 00:00:00 2001 From: Jagat Singh Date: Sat, 6 Sep 2025 03:23:29 +0530 Subject: [PATCH 4/5] Ignore uv lock --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83bbebc7..12bbe103 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: enable-cache: true - name: Install build dependencies - run: uv sync --dev + run: uv sync --dev --no-lock - name: Build package run: uv build From ca627b94846fc99c9d4afc189d3137c66461457e Mon Sep 17 00:00:00 2001 From: Jagat Singh Date: Sat, 6 Sep 2025 06:49:34 +0530 Subject: [PATCH 5/5] Update name to sqlalchemy2-redshift --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 26fe6af0..2c397416 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "sqlalchemy-redshift" +name = "sqlalchemy2-redshift" version = "0.8.15.dev0" description = "Amazon Redshift Dialect for sqlalchemy" readme = {file = "README.rst", content-type = "text/x-rst"}