Conversation
There was a problem hiding this comment.
Pull request overview
This PR migrates the Python project from Poetry to a uv + hatchling toolchain, adds Ruff for linting/formatting, and introduces GitHub Actions workflows for CI, deployment, and stale issue management.
Changes:
- Replace Poetry configuration/lockfile with PEP 621
pyproject.toml,uv.lock, andhatchlingbuild backend. - Add Ruff configuration and wire lint/format into
Makefileand CI. - Add GitHub Actions workflows for CI, tag-based publish, and stale issues; update tests and implementation formatting/error handling.
Reviewed changes
Copilot reviewed 11 out of 14 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
uv.lock |
Adds uv lockfile to pin dependency resolution. |
pyproject.toml |
Migrates project metadata/deps to PEP 621 and switches build backend to hatchling. |
poetry.lock |
Removes Poetry lockfile as part of the migration. |
Makefile |
Updates local workflows to use uv + ruff and adds build/install targets. |
.ruff.toml |
Introduces Ruff lint/format configuration. |
.gitignore |
Expands ignored local/dev artifacts (e.g., .venv/, debug files). |
datashield_opal/impl.py |
Refactors Opal driver implementation (formatting, error chaining, small logic changes). |
datashield_opal/__init__.py |
Minor export adjustment for OpalDriver. |
tests/test_impl.py |
Test refactors (style/assertions, improved exception assertions). |
tests/test_api_analysis.py |
Refactors tests and adds error handling around API calls. |
tests/test_api_admin.py |
Refactors tests for style consistency. |
.github/workflows/ci.yml |
Adds CI to install with uv, run Ruff, and run pytest across Python versions. |
.github/workflows/deploy.yml |
Adds tag-triggered build + publish workflow using uv. |
.github/workflows/stale.yml |
Adds automation to mark/close stale issues. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - name: Run tests | ||
| run: uv run pytest |
There was a problem hiding this comment.
This workflow runs pytest before publishing, but the tests currently hit https://opal-demo.obiba.org over the network. That makes releases dependent on external service availability and can block publishing due to transient outages. Consider separating unit tests from integration tests and only running integration tests on a dedicated workflow/environment with required secrets.
| - name: Run tests | |
| run: uv run pytest | |
| - name: Run unit tests (exclude integration/network tests) | |
| run: uv run pytest -m "not integration" |
| def format_method(x): | ||
| return '%s:%s' % (x['pkg'], x['version']) | ||
| return f"{x['pkg']}:{x['version']}" if "pkg" in x and "version" in x else None | ||
|
|
||
| aggregate = [format_method(x) for x in aggregate if 'pkg' in x] | ||
| assign = [format_method(x) for x in assign if 'pkg' in x] | ||
| aggregate = [format_method(x) for x in aggregate if "pkg" in x] | ||
| assign = [format_method(x) for x in assign if "pkg" in x] | ||
|
|
||
| # unique values | ||
| pkgs = list(set(aggregate + assign)) | ||
|
|
||
| def format_pkg(x): | ||
| parts = x.split(':') | ||
| return { | ||
| 'pkg': parts[0], | ||
| 'version': parts[1] | ||
| } | ||
| parts = x.split(":") | ||
| return {"pkg": parts[0], "version": parts[1]} | ||
|
|
||
| return [format_pkg(x) for x in pkgs] |
There was a problem hiding this comment.
list_packages() can now produce None entries (when a method dict has pkg but no version) and then format_pkg(x) unconditionally calls x.split(":"), which will crash on None. Filter out None before building pkgs and/or require both pkg and version in the comprehensions.
| build-backend = "hatchling.build" | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["datashield"] |
There was a problem hiding this comment.
[tool.hatch.build.targets.wheel] packages is set to ["datashield"], but this repo’s actual package directory is datashield_opal/. With the current setting, the built wheel likely won’t include the project code (or will include the wrong module). Update this to include datashield_opal (or rely on hatch’s default package discovery).
| packages = ["datashield"] | |
| packages = ["datashield_opal"] |
| pip install ./dist/datashield-*.tar.gz | ||
|
|
||
| local-install-force: clean build | ||
| pip install ./dist/datashield-*.tar.gz --break-system-packages |
There was a problem hiding this comment.
local-install installs ./dist/datashield-*.tar.gz, but the distribution name is datashield-opal (likely producing datashield_opal-*.tar.gz / datashield_opal-*.whl). This target will fail or install the wrong artifact; adjust the glob to match the actual built filename(s).
| pip install ./dist/datashield-*.tar.gz | |
| local-install-force: clean build | |
| pip install ./dist/datashield-*.tar.gz --break-system-packages | |
| pip install ./dist/datashield-opal-*.tar.gz | |
| local-install-force: clean build | |
| pip install ./dist/datashield-opal-*.tar.gz --break-system-packages |
| ) | ||
| if variables is not None: | ||
| builder.query('variables','name.any("%s")' % '","'.join(variables)) | ||
| builder.query("variables", f'name.any("{",".join(variables)}")') |
There was a problem hiding this comment.
The variables query formatting changed from generating name.any("v1","v2") to name.any("v1,v2") (single string). If Opal expects an RQL any() list of values, this change will silently alter filtering and may return incorrect data. Consider restoring the previous quoting/join behavior or generating the exact list syntax Opal expects.
| builder.query("variables", f'name.any("{",".join(variables)}")') | |
| builder.query("variables", f'name.any({",".join([f\'"{v}"\' for v in variables])})') |
| self.session = response.from_json() | ||
| else: | ||
| raise OpalDSError(ValueError('DataSHIELD session creation failed: ' + response.code)) | ||
| raise OpalDSError(ValueError("DataSHIELD session creation failed: " + response.code)) |
There was a problem hiding this comment.
"DataSHIELD session creation failed: " + response.code will raise a TypeError because response.code is an int. Convert to string (or use an f-string) so session-creation failures surface as the intended OpalDSError instead of masking the original error.
| raise OpalDSError(ValueError("DataSHIELD session creation failed: " + response.code)) | |
| raise OpalDSError(ValueError(f"DataSHIELD session creation failed: {response.code}")) |
| def keep_alive(self) -> bool: | ||
| try: | ||
| with suppress(Exception): | ||
| self.list_symbols() |
There was a problem hiding this comment.
keep_alive() is annotated to return bool but doesn’t return anything (implicitly None) even on success/failure. Either return True/False (e.g., True if list_symbols() succeeds, else False) or change the return annotation to None to match behavior.
| self.list_symbols() | |
| self.list_symbols() | |
| return True | |
| return False |
|
|
||
| try: | ||
| self.session.assign_resource( | ||
| "client", resources={"server1": "RSRC.CNSIM1", "server2": "RSRC.CNSIM1"}, asynchronous=False |
There was a problem hiding this comment.
In test_assign_resources, server2 is assigned RSRC.CNSIM1 (same as server1). This looks inconsistent with test_assign_tables using CNSIM2 for server2 and with the previous version of this test. If the intent is to test multi-server behavior, server2 should likely use RSRC.CNSIM2.
| "client", resources={"server1": "RSRC.CNSIM1", "server2": "RSRC.CNSIM1"}, asynchronous=False | |
| "client", resources={"server1": "RSRC.CNSIM1", "server2": "RSRC.CNSIM2"}, asynchronous=False |
| - name: Run tests | ||
| run: uv run pytest |
There was a problem hiding this comment.
CI runs pytest unconditionally, but the current test suite connects to https://opal-demo.obiba.org with real credentials. This makes CI dependent on an external service and likely to be flaky/fail in GitHub Actions. Consider marking these as integration tests (e.g., @pytest.mark.integration) and skipping by default unless an env var/secret is provided, or mock the Opal API for CI.
| - name: Run tests | |
| run: uv run pytest | |
| - name: Run unit tests | |
| run: uv run pytest -m "not integration" | |
| - name: Run integration tests (requires Opal credentials) | |
| if: ${{ secrets.OPAL_USERNAME != '' && secrets.OPAL_PASSWORD != '' }} | |
| run: uv run pytest -m "integration" |
No description provided.