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
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.2"
version: "0.9.17"
- name: Create virtual environment
run: uv sync --only-dev
- name: Publish the docs to GitHub Pages
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.2"
version: "0.9.17"
- name: Build source dist and wheels
run: uv build
- name: Upload source dist and wheels to artifacts
Expand Down Expand Up @@ -58,7 +58,7 @@ jobs:
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.2"
version: "0.9.17"
- name: Publish source dist and wheels to PyPI
run: uv publish

Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.2"
version: "0.9.17"
- name: Create virtual environment
run: uv sync --only-dev
- name: Publish the docs to GitHub Pages
Expand Down
30 changes: 29 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,34 @@ jobs:
- name: Run pre-commit hooks
uses: pre-commit/action@v3.0.1

test:
needs: pre-commit
runs-on: ubuntu-24.04
strategy:
matrix:
python_version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
steps:
- name: Clone full tree, and checkout branch
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "${{ matrix.python_version }}"
cache: "pip"
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.17"
- name: Run tests
run: uv run poe test

build:
needs: pre-commit
runs-on: ubuntu-24.04
Expand All @@ -46,7 +74,7 @@ jobs:
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.2"
version: "0.9.17"
- name: Build source dist and wheels
run: uv build
- name: Upload source dist and wheels to artifacts
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
rspec.xml

# Translations
*.mo
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ repos:
- id: check-added-large-files
- id: check-merge-conflict
- repo: https://github.com/crate-ci/typos
rev: "v1.38.1"
rev: "v1.40.0"
hooks:
- id: typos
- repo: https://github.com/astral-sh/uv-pre-commit
rev: "0.9.2"
rev: "0.9.17"
hooks:
- id: uv-lock
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.14.0"
rev: "v0.14.9"
hooks:
- id: ruff-check
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.18.2"
rev: "v1.19.0"
hooks:
- id: mypy
additional_dependencies:
Expand Down
1 change: 1 addition & 0 deletions changelog.d/13.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Replace supplementary record/manager classes with mixins
226 changes: 225 additions & 1 deletion docs/managers/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ from datetime import datetime
from openstack_odooclient import RecordBase

class CustomRecord(RecordBase["CustomRecordManager"]):
custom_field: date
custom_field: datetime
"""Description of the field."""
```

Expand Down Expand Up @@ -791,6 +791,230 @@ The following internal attributes are also available for use in methods:
* `_odoo` (`odoorpc.ODOO`) - The OdooRPC connection object
* `_env` (`odoorpc.env.Environment`) - The OdooRPC environment object for the model

## Mixins

Python supports [multiple inheritance](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance)
when creating new classes. A common use case for multiple inheritance is to extend
functionality of a class through the use of *mixin classes*, which are minimal
classes that only consist of supplementary attributes and methods, that get added
to other classes through subclassing.

The OpenStack Odoo Client library for Python supports the use of mixin classes
to add functionality to custom record and manager classes in a modular way.
Multiple mixins can be added to record and manager classes to allow mixing and
matching additional functionality as required.

### Using Mixins

To extend the functionality of your custom record and manager classes,
append the mixins for the record class and/or record manager class
**AFTER** the inheritance for `RecordBase` and `RecordManagerBase`.
You also need to specify the **same** type arguments to the mixins as
is already being done for `RecordBase` and `RecordManagerBase`.

```python
from __future__ import annotations

from openstack_odooclient import (
NamedRecordManagerMixin,
NamedRecordMixin,
RecordBase,
RecordManagerBase,
)

class CustomRecord(
RecordBase["CustomRecordManager"],
NamedRecordMixin["CustomRecordManager"],
):
custom_field: str
"""Description of the field."""

class CustomRecordManager(
RecordManagerBase[CustomRecord],
NamedRecordManagerMixin[CustomRecord],
):
env_name = "custom.record"
record_class = CustomRecord
```

That's all that needs to be done. The additional attributes and/or methods
should now be available on your record and manager objects.

The following mixins are provided with the Odoo Client library.

#### Named Records

If your record model has a unique `name` field on it (of `str` type),
you can use the `NamedRecordMixin` and `NamedRecordManagerMixin` mixins
to define the `name` field on the record class, and add the
`get_by_name` method to your custom record manager class.

```python
from __future__ import annotations

from openstack_odooclient import (
NamedRecordManagerMixin,
NamedRecordMixin,
RecordBase,
RecordManagerBase,
)

class CustomRecord(
RecordBase["CustomRecordManager"],
NamedRecordMixin["CustomRecordManager"],
):
custom_field: str
"""Description of the field."""

# Added by NamedRecordMixin:
#
# name: str
# """The unique name of the record."""

class CustomRecordManager(
RecordManagerBase[CustomRecord],
NamedRecordManagerMixin[CustomRecord],
):
env_name = "custom.record"
record_class = CustomRecord

# Added by NamedRecordManagerMixin:
#
# def get_by_name(...):
# ...
```

For more information on using record managers with unique `name` fields,
see [Named Record Managers](index.md#named-record-managers).

#### Coded Records

If your record model has a unique `code` field on it (of `str` type),
you can use the `CodedRecordMixin` and `CodedRecordManagerMixin` mixins
to define the `code` field on the record class, and add the
`get_by_code` method to your custom record manager class.

```python
from __future__ import annotations

from openstack_odooclient import (
CodedRecordManagerMixin,
CodedRecordMixin,
RecordBase,
RecordManagerBase,
)

class CustomRecord(
RecordBase["CustomRecordManager"],
CodedRecordMixin["CustomRecordManager"],
):
custom_field: str
"""Description of the field."""

# Added by CodedRecordMixin:
#
# code: str
# """The unique name for this record."""

class CustomRecordManager(
RecordManagerBase[CustomRecord],
CodedRecordManagerMixin[CustomRecord],
):
env_name = "custom.record"
record_class = CustomRecord

# Added by CodedRecordManagerMixin:
#
# def get_by_code(...):
# ...
```

For more information on using record managers with unique `code` fields,
see [Coded Record Managers](index.md#coded-record-managers).

### Creating Mixins

It is possible to create your own custom mixins to incorporate into
custom record and manager classes.

There are two mixin types: **record mixins** and **record manager mixins**.

#### Record Mixins

Record mixins are used to add custom fields and methods to record classes.

Here is the full implementation of `NamedRecordMixin` as an example
of a mixin for a record class, that simply adds the `name` field:

```python
from __future__ import annotations

from typing import Generic

from openstack_odooclient import RM, RecordProtocol

class NamedRecordMixin(RecordProtocol[RM], Generic[RM]):
name: str
"""The unique name of the record."""
```

A record mixin consists of a class that subclasses `RecordProtocol[RM]`
(where `RM` is the type variable for a record manager class) to get the type
hints for a record class' common fields and methods. `Generic[RM]` is also
subclassed to make the mixin itself a generic class, to allow `RM` to be
passed when creating a record class with the mixin.

Once you have the class, simply define any fields and methods you'd like
to add.

You can then use the mixin as shown in [Using Mixins](#using-mixins).

When defining custom methods, in addition to accessing fields/methods
defined within the mixin, fields/methods from the `RecordBase` class
are also available:

```python
from __future__ import annotations

from typing import Generic

from openstack_odooclient import RM, RecordProtocol

class NamedRecordMixin(RecordProtocol[RM], Generic[RM]):
name: str
"""The unique name of the record."""

def custom_method(self) -> None:
self.name # str
self._env.custom_method(self.id)
```

#### Record Manager Mixins

Record manager mixins are expected to be mainly used to add custom methods
to a record manager class.

```python
from __future__ import annotations

from typing import Generic

from openstack_odooclient import R, RecordManagerProtocol

class NamedRecordManagerMixin(RecordManagerProtocol[R], Generic[R]):
def custom_method(self, record: int | R) -> None:
self._env.custom_method( # self._env available from RecordManagerBase
record if isinstance(record, int) else record.id,
)
```

A record manager mixin consists of a class that subclasses
`RecordManagerProtocol[R]` (where `R` is the type variable for a record class)
to get the type hints for a record manager class' common attributes and
methods. `Generic[R]` is also subclassed to make the mixin itself a generic
class, to allow `R` to be passed when creating a record manager class
with the mixin.

## Extending Existing Record Types

The Odoo Client library provides *limited* support for extending the built-in record types.
Expand Down
2 changes: 0 additions & 2 deletions docs/managers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -873,9 +873,7 @@ The managers for these record types have additional methods for querying records
* [Currencies](currency.md)
* [OpenStack Customer Groups](customer-group.md)
* [OpenStack Grant Types](grant-type.md)
* [Partner Categories](partner-category.md)
* [Pricelists](pricelist.md)
* [Product Categories](product-category.md)
* [OpenStack Reseller Tiers](reseller-tier.md)
* [Sale Orders](sale-order.md)
* [OpenStack Support Subscription Types](support-subscription-type.md)
Expand Down
2 changes: 2 additions & 0 deletions docs/managers/partner-category.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ name: str

The name of the partner category.

Not guaranteed to be unique, even under the same parent category.

### `parent_id`

```python
Expand Down
4 changes: 3 additions & 1 deletion docs/managers/product-category.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ The complete product category tree.
name: str
```

Name of the product category.
The name of the product category.

Not guaranteed to be unique, even under the same parent category.

### `parent_id`

Expand Down
2 changes: 1 addition & 1 deletion docs/managers/voucher-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ until it expires.
name: str
```

The unique name of this voucher code.
The automatically generated name of this voucher code.

This uses the code specified in the record as-is.

Expand Down
Loading