Skip to content

feat: add get_lockfile() method to ContentItem#444

Merged
tdstein merged 3 commits intomainfrom
feature/content-lockfile-endpoint
Jan 21, 2026
Merged

feat: add get_lockfile() method to ContentItem#444
tdstein merged 3 commits intomainfrom
feature/content-lockfile-endpoint

Conversation

@tdstein
Copy link
Copy Markdown
Collaborator

@tdstein tdstein commented Jan 16, 2026

Summary

Adds support for retrieving Python lockfiles (requirements.txt.lock) from content items with managed Python environments.

This implements the GET /v1/content/{guid}/lockfile endpoint that was added in Connect 2025.12.0 (posit-dev/connect#35619).

Changes

Implementation

  • New method: ContentItem.get_lockfile() returns the Python lockfile as a string
  • Version requirement: @requires(version="2025.12.0") enforces minimum Connect version
  • Error handling: Raises ClientError when content doesn't have managed Python environment

Tests

  • Unit tests (3): Success case, version check, and error handling
  • Integration tests (2): End-to-end validation with real Connect deployment
  • Updated integration Makefile: Added Connect 2025.12.0 to test matrix

Usage

from posit import connect

client = connect.Client()
content = client.content.get(guid)

# Get the Python lockfile
lockfile = content.get_lockfile()
print(lockfile)
# Output: requirements.txt.lock content with pinned package versions

Test Coverage

All tests passing:

  • ✅ 373 unit tests (including 3 new tests for get_lockfile)
  • ✅ Integration tests verified manually (will run automatically in CI on Connect 2025.12.0)
  • ✅ Linting (ruff) and type checking (pyright) pass

Related

  • Connect API docs PR: posit-dev/connect#35619
  • Connect endpoint implementation: posit-dev/connect#34867

🤖 Generated with Claude Code

Add support for retrieving Python lockfiles (requirements.txt.lock) from
content items with managed Python environments.

This implements the GET /v1/content/{guid}/lockfile endpoint that was
added in Connect 2025.12.0.

Changes:
- Add ContentItem.get_lockfile() method with @requires(version="2025.12.0")
- Add 3 unit tests for success, version check, and error handling
- Add 2 integration tests for end-to-end validation
- Update integration Makefile to include Connect 2025.12.0

The method returns the Python lockfile as a string, which describes the
exact Python packages installed in Connect's managed environment.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 16, 2026

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
2326 2175 94% 0% 🟢

New Files

No new covered files...

Modified Files

File Coverage Status
src/posit/connect/content.py 99% 🟢
TOTAL 99% 🟢

updated for commit: b5fb2e0 by action🐍

@tdstein tdstein requested a review from amol- January 16, 2026 15:04
# that the method exists and is callable on supported versions
content = self.client.content.create(name="example-version-check")
path = Path("../../../resources/connect/bundles/example-flask-minimal/bundle.tar.gz")
path = (Path(__file__).parent / path).resolve()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to see an helper in the test class for this "Path dance" (like self.example_bundle_path('example-flask-minimal'), as it's repeated in every test, but it's definitely out of scope.

"""
path = f"v1/content/{self['guid']}/lockfile"
response = self._ctx.client.get(path)
return response.text
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might want to return the Generated-By field too , as that contains the Python version for which the lockfile works, which is a necessary information to reuse the lockfile.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the client side workflow look like for this? Maybe that can help inform what this method returns?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rsconnect-python for example uses that information to create a new local virtual environment compatible with the deployed content, see https://github.com/posit-dev/rsconnect-python/blob/13b63b56a92c713172cfbaa2f19754492efec51a/rsconnect/main.py#L3185-L3204

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tdstein I think the issue reported on Generated-By is stil pending right?
Is there a reason why we can't simply return a turple from the controller with both response.text and the generated-by value? I think that's more or less all we need in this context.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added two commits:

e7b1c84
uses a tuple to return the raw value of generated_by like you suggested

b5fb2e0 iterates further by adding a dataclass which parses out the python version for more convientally access.

Let me know which you prefer. Generally, I try to avoid using tuples in public sdks like this since (imo) they are harder to change without breaking downstream users.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding dataclass I'm usually not very fond of the idea of polluting the namespace with additional entities that bring no behavior and introduce additional levels of indirection for the data, but I understand that tuples are harder to migrate on long term.
I personally would have used a namedtuple in general, but I think that in this case, as we are also introducing helpers to parse the Generated-By header the Lockfile class implementation is the one that provides most value.

Flagging the PR as reviewed 👍

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I always forget about namedtuple! I guess that shows when I started programming in Python...

tdstein and others added 2 commits January 21, 2026 10:12
The method now returns (generated_by, lockfile_content) and raises
ValueError if the server response is missing the Generated-By header.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduces a Lockfile dataclass that provides convenient access to:
- generated_by: the raw Generated-By header value
- python_version: parsed Python version (e.g., "3.11.4")
- text: the lockfile content

Also adds a write() method to easily save the lockfile to disk.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@tdstein tdstein requested a review from amol- January 21, 2026 15:37
@tdstein tdstein merged commit 316117b into main Jan 21, 2026
55 checks passed
@tdstein tdstein deleted the feature/content-lockfile-endpoint branch January 21, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants