Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/actions/publish/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ runs:
shell: bash

- name: Setup python
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.PYTHON_VERSION }}
run: uv python install ${{ inputs.PYTHON_VERSION }}
shell: bash

- name: Build package
run: uv build
Expand Down
13 changes: 2 additions & 11 deletions .github/actions/test-docs/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@ inputs:
description: Python version
required: false
default: "3.13"
POSTGRES_VERSION:
description: Postgres major version to use
required: false
default: 12
CONTAINER_NETWORK:
description: Docker container network to use
required: false
default: pgmob-network

runs:
using: "composite"
Expand All @@ -33,9 +25,8 @@ runs:
shell: bash

- name: Setup python
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.PYTHON_VERSION }}
run: uv python install ${{ inputs.PYTHON_VERSION }}
shell: bash

- name: Install dependencies
run: uv sync
Expand Down
119 changes: 119 additions & 0 deletions .github/workflows/auto-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Auto Release from Changelog

on:
push:
branches:
- main
paths:
- 'CHANGELOG.md'

jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Parse Changelog
id: changelog
run: |
# Extract the latest version and its content from CHANGELOG.md
python3 << 'EOF'
import re
import sys

with open('CHANGELOG.md', 'r') as f:
content = f.read()

# Match version headers like ## [0.3.1] - 2026-02-10
version_pattern = r'^## \[([^\]]+)\] - (\d{4}-\d{2}-\d{2})'
matches = list(re.finditer(version_pattern, content, re.MULTILINE))

if not matches:
print("No version found in CHANGELOG.md", file=sys.stderr)
sys.exit(1)

# Get the first (latest) version
first_match = matches[0]
version = first_match.group(1)
date = first_match.group(2)

# Extract content between first and second version headers
start_pos = first_match.end()
if len(matches) > 1:
end_pos = matches[1].start()
body = content[start_pos:end_pos].strip()
else:
# If only one version, get everything after it until the end or separator
remaining = content[start_pos:]
separator_match = re.search(r'^---$', remaining, re.MULTILINE)
if separator_match:
body = remaining[:separator_match.start()].strip()
else:
body = remaining.strip()

# Clean up the body - remove leading/trailing whitespace
body = body.strip()

# Write outputs
with open('version.txt', 'w') as f:
f.write(version)
with open('body.txt', 'w') as f:
f.write(body)

print(f"Version: {version}")
print(f"Date: {date}")
print(f"Body length: {len(body)} characters")
EOF

# Set outputs
VERSION=$(cat version.txt)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT

# For multiline output, use delimiter
{
echo 'body<<EOF_CHANGELOG'
cat body.txt
echo EOF_CHANGELOG
} >> $GITHUB_OUTPUT

- name: Check if release exists
id: check_release
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.changelog.outputs.tag }}"

# Check if release exists
if gh release view "$TAG" &>/dev/null; then
echo "Release $TAG already exists"
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "Release $TAG does not exist"
echo "exists=false" >> $GITHUB_OUTPUT
fi

- name: Create Release
if: steps.check_release.outputs.exists == 'false'
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.changelog.outputs.tag }}"
VERSION="${{ steps.changelog.outputs.version }}"

# Create release with changelog body
gh release create "$TAG" \
--title "Release $VERSION" \
--notes "${{ steps.changelog.outputs.body }}" \
--verify-tag

- name: Skip Release
if: steps.check_release.outputs.exists == 'true'
run: |
echo "Release ${{ steps.changelog.outputs.tag }} already exists. Skipping creation."
1 change: 1 addition & 0 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
paths:
- 'docs/**'
- '.github/**'
- 'src/pgmob/**'
pull_request:
branches:
- main
Expand Down
6 changes: 0 additions & 6 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ jobs:
uses: actions/checkout@v3
- name: Run tests
uses: ./.github/actions/test
# - name: Bump version
# uses: ./.github/actions/bump-version
# with:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# GIT_USERNAME: ${{ secrets.GIT_USERNAME }}
# GIT_EMAIL: ${{ secrets.GIT_EMAIL }}
- name: Publish to PyPI Test
uses: ./.github/actions/publish
with:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dist/
.DS_Store
.mypy_cache/
.pytest_cache/
.hypothesis/

# coverage
artifacts/
Expand All @@ -19,3 +20,6 @@ coverage.xml
# docs build
docs/_build
.vscode/launch.json

# IDEs
.kiro/specs/
144 changes: 144 additions & 0 deletions .windsurf/rules/api-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,150 @@ def owner(self, value: str) -> None:
generic._set_ephemeral_attr(self, "owner", value)
```

### Mixin Properties

Mixin properties follow the same API standards as regular properties. They provide consistent getter/setter behavior with change tracking.

#### Mixin Property Docstrings

Mixin properties MUST include docstrings that describe the property:

```python
class NamedObjectMixin:
"""Mixin providing name property with change tracking."""

@property
def name(self) -> str:
"""The object's name.

Returns:
The name of the database object.
"""
return self._name

@name.setter
def name(self, value: str) -> None:
"""Set the object's name.

Changes are queued and applied when alter() is called.

Args:
value: New name for the object.

Example:
>>> table.name = "new_table_name"
>>> table.alter() # Apply the name change
"""
from . import generic
generic._set_ephemeral_attr(self, "name", value)
```

#### Mixin Initialization in Object __init__

Objects using mixins MUST call mixin initialization methods in their `__init__`:

```python
class Table(
NamedObjectMixin,
OwnedObjectMixin,
SchemaObjectMixin,
_DynamicObject,
_CollectionChild
):
"""Postgres Table object.

Args:
name: Table name
schema: Schema name (default: 'public')
owner: Table owner
cluster: Postgres cluster object
parent: Parent collection
oid: Table OID

Attributes:
name: Table name (from NamedObjectMixin)
owner: Table owner (from OwnedObjectMixin)
schema: Schema name (from SchemaObjectMixin)
tablespace: Tablespace (from TablespaceObjectMixin)
row_security: Whether row security is enabled
oid: Table OID
"""

def __init__(
self,
name: str,
schema: str = "public",
owner: Optional[str] = None,
cluster: "Cluster" = None,
parent: "TableCollection" = None,
oid: Optional[int] = None
):
# Initialize base classes
super().__init__(kind="TABLE", cluster=cluster, oid=oid, name=name, schema=schema)
_CollectionChild.__init__(self, parent=parent)

# Initialize mixins - REQUIRED for mixin properties to work
self._init_name(name)
self._init_owner(owner)
self._init_schema(schema)
self._init_tablespace(None)

# Table-specific attributes
self._row_security: bool = False
```

#### Documenting Mixin Properties in Class Docstrings

When a class uses mixins, document which properties come from mixins in the class docstring:

```python
class Sequence(
NamedObjectMixin,
OwnedObjectMixin,
SchemaObjectMixin,
_DynamicObject,
_CollectionChild
):
"""Postgres sequence object.

Attributes:
name: Sequence name (from NamedObjectMixin)
owner: Sequence owner (from OwnedObjectMixin)
schema: Schema name (from SchemaObjectMixin)
data_type: Data type of the sequence
start_value: Starting value
increment_by: Increment value
min_value: Minimum value
max_value: Maximum value
"""
```

#### Mixin Property Behavior

Mixin properties behave identically to regular properties:

- **Getters**: Return the current value
- **Setters**: Queue changes via `_set_ephemeral_attr()` for later application
- **Change tracking**: Changes are stored in `self._changes` dictionary
- **Application**: Changes are applied when `alter()` is called

```python
# Using mixin properties
table = cluster.tables["users"]

# Get property (from NamedObjectMixin)
print(table.name) # "users"

# Set property (queues change)
table.name = "app_users"

# Property is updated locally
print(table.name) # "app_users"

# Apply change to database
table.alter() # Executes: ALTER TABLE users RENAME TO app_users
```

### Lazy Properties
```python
from ._decorators import get_lazy_property
Expand Down
Loading