extensionmethods 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,133 @@
1
+ name: Build, unittest and publish
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ tags:
8
+ - "v[0-9]+.[0-9]+.[0-9]+"
9
+ workflow_dispatch:
10
+
11
+ env:
12
+ UV_VERSION: "0.9.7"
13
+
14
+ jobs:
15
+ build:
16
+ name: Build package
17
+ runs-on: "ubuntu-latest"
18
+
19
+ steps:
20
+ - name: Checkout repository
21
+ uses: actions/checkout@v4
22
+ with:
23
+ fetch-depth: 0 # required for setuptools-scm
24
+
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v6
27
+ with:
28
+ version: ${{ env.UV_VERSION }}
29
+
30
+ - name: Set up Python
31
+ run: uv python install
32
+
33
+ - name: Install the project
34
+ run: uv sync --locked --dev
35
+
36
+ - name: Build packages
37
+ run: uv build --all-packages
38
+
39
+ - name: Store the distribution packages
40
+ uses: actions/upload-artifact@v5
41
+ with:
42
+ name: python-package-distributions
43
+ path: dist/
44
+
45
+ test:
46
+ name: Test package
47
+ runs-on: "ubuntu-latest"
48
+ needs:
49
+ - build
50
+ strategy:
51
+ matrix:
52
+ python-version:
53
+ - "3.11"
54
+ - "3.12"
55
+ - "3.13"
56
+
57
+ steps:
58
+ - name: Checkout repository
59
+ uses: actions/checkout@v4
60
+
61
+ - name: Install uv
62
+ uses: astral-sh/setup-uv@v6
63
+ with:
64
+ version: ${{ env.UV_VERSION }}
65
+ python-version: ${{ matrix.python-version }}
66
+
67
+ - name: Set up Python
68
+ run: uv python install
69
+
70
+ - name: Download all the dists
71
+ uses: actions/download-artifact@v6
72
+ with:
73
+ name: python-package-distributions
74
+ path: dist/
75
+
76
+ - name: Install package
77
+ run: |
78
+ uv venv
79
+ uv pip install dist/*.whl
80
+ uv pip install pytest
81
+
82
+ - name: Test with pytest
83
+ run: |
84
+ uv run pytest
85
+
86
+ publish-testpypi:
87
+ name: Publish to TestPyPI
88
+ if: startsWith(github.ref, 'refs/tags/') # only publish on tag pushes
89
+ needs:
90
+ - test
91
+ runs-on: ubuntu-latest
92
+
93
+ environment:
94
+ name: testpypi
95
+ url: https://test.pypi.org/p/extensionmethods
96
+
97
+ permissions:
98
+ id-token: write # IMPORTANT: mandatory for trusted publishing
99
+
100
+ steps:
101
+ - name: Download all the dists
102
+ uses: actions/download-artifact@v6
103
+ with:
104
+ name: python-package-distributions
105
+ path: dist/
106
+ - name: Publish distribution to TestPyPI
107
+ uses: pypa/gh-action-pypi-publish@release/v1
108
+ with:
109
+ repository-url: https://test.pypi.org/legacy/
110
+ verbose: true
111
+
112
+ publish-pypi:
113
+ name: Publish to PyPI
114
+ if: startsWith(github.ref, 'refs/tags/') # only publish on tag pushes
115
+ needs:
116
+ - publish-testpypi
117
+ runs-on: ubuntu-latest
118
+
119
+ environment:
120
+ name: pypi
121
+ url: https://pypi.org/p/extensionmethods
122
+
123
+ permissions:
124
+ id-token: write # IMPORTANT: mandatory for trusted publishing
125
+
126
+ steps:
127
+ - name: Download all the dists
128
+ uses: actions/download-artifact@v6
129
+ with:
130
+ name: python-package-distributions
131
+ path: dist/
132
+ - name: Publish to PyPI
133
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1,11 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ # Ruff version.
4
+ rev: v0.11.2 # Same as VS Code extension
5
+ hooks:
6
+ # Run the linter.
7
+ - id: ruff
8
+ args:
9
+ - --fix
10
+ # Run the formatter.
11
+ - id: ruff-format
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,5 @@
1
+ {
2
+ "recommendations": [
3
+ "charliermarsh.ruff"
4
+ ]
5
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "python.analysis.typeCheckingMode": "standard",
3
+ "editor.codeActionsOnSave": {
4
+ "source.fixAll": "always",
5
+ "source.organizeImports.ruff": "always"
6
+ },
7
+ "python.testing.pytestArgs": [
8
+ "tests"
9
+ ],
10
+ "python.testing.unittestEnabled": false,
11
+ "python.testing.pytestEnabled": true
12
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pim Mostert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: extensionmethods
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author-email: Pim Mostert <pim.mostert@pimmostert.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Pim-Mostert/extensionmethods
8
+ Project-URL: Issues, https://github.com/Pim-Mostert/extensionmethods/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # extensionmethods
17
+
18
+ Mimics C#-style extension methods in Python.
19
+
20
+ `extensionmethods` is a tiny package that lets you “attach” functions to existing types without modifying their source code, enabling method-like syntax, better chaining, and cleaner separation of optional dependencies.
21
+
22
+ - [extensionmethods](#extensionmethods)
23
+ - [Example usage](#example-usage)
24
+ - [Type safety and type checking](#type-safety-and-type-checking)
25
+ - [Installation](#installation)
26
+ - [Why use extension methods?](#why-use-extension-methods)
27
+ - [Readability through chaining](#readability-through-chaining)
28
+ - [Modularity and dependency isolation](#modularity-and-dependency-isolation)
29
+ - [Known caveats](#known-caveats)
30
+ - [IDE type hints may be misleading](#ide-type-hints-may-be-misleading)
31
+ - [Uses the `|` operator (`__ror__`)](#uses-the--operator-__ror__)
32
+ - [License](#license)
33
+
34
+
35
+ ## Example usage
36
+
37
+ In C#, you can add methods to existing types:
38
+
39
+ ```csharp
40
+ public static class IntExtensions
41
+ {
42
+ public static int Double(this int x)
43
+ {
44
+ return x * 2;
45
+ }
46
+ }
47
+
48
+ int result = 7.Double();
49
+ ```
50
+
51
+ You get method syntax without modifying `int`.
52
+
53
+ With the `extensionmethods` package you can achieve similiar functionality like so:
54
+
55
+ ```python
56
+ from extensionmethods import extension
57
+
58
+ @extension(to=int)
59
+ def double(x: int) -> int:
60
+ return x * 2
61
+
62
+ result = 7 | double()
63
+ print(result) # 14
64
+ ```
65
+
66
+ With parameters:
67
+
68
+ ```python
69
+ @extension(to=int)
70
+ def add_then_multiply(x: int, to_add: int, to_multiply: int) -> int:
71
+ return (x + to_add) * to_multiply
72
+
73
+ result = 7 | add_then_multiply(11, 3)
74
+ print(result) # 54
75
+ ```
76
+
77
+ The value on the left side becomes the first argument of the function.
78
+
79
+ ## Type safety and type checking
80
+
81
+ The extension methods are type-aware.
82
+
83
+ When you declare an extension, you bind it to a specific type:
84
+
85
+ ```python
86
+ @extension(to=int)
87
+ def double(x: int) -> int:
88
+ return x * 2
89
+ ```
90
+
91
+ This gives you safety at two levels:
92
+
93
+ - **IDE / static type checking**
94
+ Type checkers and editors can detect incorrect usage:
95
+
96
+ ```python
97
+ "hello" | double() # type error
98
+ ```
99
+
100
+ Your IDE (e.g. VS Code) can flag this because the extension is declared for `int`, not `str`.
101
+
102
+ - **Runtime enforcement**
103
+
104
+ Even if type checking is bypassed, the library validates the type at runtime:
105
+
106
+ ```python
107
+ >>> "hello" | double()
108
+ TypeError: Extension 'double' can only be used on 'int', not 'str'
109
+ ```
110
+
111
+ ## Installation
112
+
113
+ Using **pip**:
114
+
115
+ ```bash
116
+ pip install extensionmethods
117
+ ```
118
+
119
+ Using **uv**:
120
+
121
+ ```bash
122
+ uv pip install extensionmethods
123
+ ```
124
+
125
+ ## Why use extension methods?
126
+
127
+ ### Readability through chaining
128
+
129
+ Instead of nested calls:
130
+
131
+ ```python
132
+ result = normalize(scale(center(data)))
133
+ ```
134
+
135
+ You can express the same flow step-by-step:
136
+
137
+ ```python
138
+ result = data | center() | scale() | normalize()
139
+ ```
140
+
141
+ This reads left-to-right and mirrors how data is conceptually transformed.
142
+
143
+ ### Modularity and dependency isolation
144
+
145
+ Suppose you maintain a core class:
146
+
147
+ ```python
148
+ class Dataset:
149
+ ...
150
+ ```
151
+
152
+ You want export helpers:
153
+
154
+ - `to_pandas()`
155
+ - `to_numpy()`
156
+ - `to_torch()`
157
+
158
+ If you put these methods directly on `Dataset`, your core package must depend on `pandas`, `numpy`, and `torch`.
159
+
160
+ Instead, keep the core dependency-free:
161
+
162
+ ```python
163
+ # core package
164
+ class Dataset:
165
+ ...
166
+ ```
167
+
168
+ Then provide optional extensions:
169
+
170
+ ```python
171
+ # dataset_pandas package
172
+ import pandas as pd
173
+ from extensionmethods import extension
174
+ from core import Dataset
175
+
176
+ @extension(to=Dataset)
177
+ def to_pandas(ds: Dataset) -> pd.DataFrame:
178
+ ...
179
+ ```
180
+
181
+ Usage:
182
+
183
+ ```python
184
+ import dataset_pandas # registers the extension
185
+
186
+ df = dataset | to_pandas()
187
+ ```
188
+
189
+ Now:
190
+
191
+ - The core package has zero heavy dependencies
192
+ - Users only install what they need
193
+ - Functionality stays logically grouped
194
+
195
+ ## Known caveats
196
+
197
+ ### IDE type hints may be misleading
198
+
199
+ Editors like VS Code may show hover/type information for the decorator wrapper, not the original function.
200
+
201
+ ```python
202
+ @extension(to=int)
203
+ def double(x: int) -> int:
204
+ return x * 2
205
+ ```
206
+
207
+ Hovering `double` may not show the expected signature `(x: int) -> int`, but instead `(function) double: ExtensionDecoratorFactory[int]`.
208
+
209
+ ### Uses the `|` operator (`__ror__`)
210
+
211
+ The system works by overriding the right-side bitwise OR operator.
212
+
213
+ ```python
214
+ result = value | extension_call()
215
+ ```
216
+
217
+ This only works if the left-hand type does not fully consume the `|` operator itself.
218
+
219
+ For example, sets already use `|`:
220
+
221
+ ```python
222
+ {1, 2} | {3} # set union
223
+ ```
224
+
225
+ If a type defines its own `__or__` in a way that prevents fallback to `__ror__`, the extension method will not run.
226
+
227
+ ## License
228
+
229
+ This project is licensed under the MIT License. See the `LICENSE` file for details.
@@ -0,0 +1,214 @@
1
+ # extensionmethods
2
+
3
+ Mimics C#-style extension methods in Python.
4
+
5
+ `extensionmethods` is a tiny package that lets you “attach” functions to existing types without modifying their source code, enabling method-like syntax, better chaining, and cleaner separation of optional dependencies.
6
+
7
+ - [extensionmethods](#extensionmethods)
8
+ - [Example usage](#example-usage)
9
+ - [Type safety and type checking](#type-safety-and-type-checking)
10
+ - [Installation](#installation)
11
+ - [Why use extension methods?](#why-use-extension-methods)
12
+ - [Readability through chaining](#readability-through-chaining)
13
+ - [Modularity and dependency isolation](#modularity-and-dependency-isolation)
14
+ - [Known caveats](#known-caveats)
15
+ - [IDE type hints may be misleading](#ide-type-hints-may-be-misleading)
16
+ - [Uses the `|` operator (`__ror__`)](#uses-the--operator-__ror__)
17
+ - [License](#license)
18
+
19
+
20
+ ## Example usage
21
+
22
+ In C#, you can add methods to existing types:
23
+
24
+ ```csharp
25
+ public static class IntExtensions
26
+ {
27
+ public static int Double(this int x)
28
+ {
29
+ return x * 2;
30
+ }
31
+ }
32
+
33
+ int result = 7.Double();
34
+ ```
35
+
36
+ You get method syntax without modifying `int`.
37
+
38
+ With the `extensionmethods` package you can achieve similiar functionality like so:
39
+
40
+ ```python
41
+ from extensionmethods import extension
42
+
43
+ @extension(to=int)
44
+ def double(x: int) -> int:
45
+ return x * 2
46
+
47
+ result = 7 | double()
48
+ print(result) # 14
49
+ ```
50
+
51
+ With parameters:
52
+
53
+ ```python
54
+ @extension(to=int)
55
+ def add_then_multiply(x: int, to_add: int, to_multiply: int) -> int:
56
+ return (x + to_add) * to_multiply
57
+
58
+ result = 7 | add_then_multiply(11, 3)
59
+ print(result) # 54
60
+ ```
61
+
62
+ The value on the left side becomes the first argument of the function.
63
+
64
+ ## Type safety and type checking
65
+
66
+ The extension methods are type-aware.
67
+
68
+ When you declare an extension, you bind it to a specific type:
69
+
70
+ ```python
71
+ @extension(to=int)
72
+ def double(x: int) -> int:
73
+ return x * 2
74
+ ```
75
+
76
+ This gives you safety at two levels:
77
+
78
+ - **IDE / static type checking**
79
+ Type checkers and editors can detect incorrect usage:
80
+
81
+ ```python
82
+ "hello" | double() # type error
83
+ ```
84
+
85
+ Your IDE (e.g. VS Code) can flag this because the extension is declared for `int`, not `str`.
86
+
87
+ - **Runtime enforcement**
88
+
89
+ Even if type checking is bypassed, the library validates the type at runtime:
90
+
91
+ ```python
92
+ >>> "hello" | double()
93
+ TypeError: Extension 'double' can only be used on 'int', not 'str'
94
+ ```
95
+
96
+ ## Installation
97
+
98
+ Using **pip**:
99
+
100
+ ```bash
101
+ pip install extensionmethods
102
+ ```
103
+
104
+ Using **uv**:
105
+
106
+ ```bash
107
+ uv pip install extensionmethods
108
+ ```
109
+
110
+ ## Why use extension methods?
111
+
112
+ ### Readability through chaining
113
+
114
+ Instead of nested calls:
115
+
116
+ ```python
117
+ result = normalize(scale(center(data)))
118
+ ```
119
+
120
+ You can express the same flow step-by-step:
121
+
122
+ ```python
123
+ result = data | center() | scale() | normalize()
124
+ ```
125
+
126
+ This reads left-to-right and mirrors how data is conceptually transformed.
127
+
128
+ ### Modularity and dependency isolation
129
+
130
+ Suppose you maintain a core class:
131
+
132
+ ```python
133
+ class Dataset:
134
+ ...
135
+ ```
136
+
137
+ You want export helpers:
138
+
139
+ - `to_pandas()`
140
+ - `to_numpy()`
141
+ - `to_torch()`
142
+
143
+ If you put these methods directly on `Dataset`, your core package must depend on `pandas`, `numpy`, and `torch`.
144
+
145
+ Instead, keep the core dependency-free:
146
+
147
+ ```python
148
+ # core package
149
+ class Dataset:
150
+ ...
151
+ ```
152
+
153
+ Then provide optional extensions:
154
+
155
+ ```python
156
+ # dataset_pandas package
157
+ import pandas as pd
158
+ from extensionmethods import extension
159
+ from core import Dataset
160
+
161
+ @extension(to=Dataset)
162
+ def to_pandas(ds: Dataset) -> pd.DataFrame:
163
+ ...
164
+ ```
165
+
166
+ Usage:
167
+
168
+ ```python
169
+ import dataset_pandas # registers the extension
170
+
171
+ df = dataset | to_pandas()
172
+ ```
173
+
174
+ Now:
175
+
176
+ - The core package has zero heavy dependencies
177
+ - Users only install what they need
178
+ - Functionality stays logically grouped
179
+
180
+ ## Known caveats
181
+
182
+ ### IDE type hints may be misleading
183
+
184
+ Editors like VS Code may show hover/type information for the decorator wrapper, not the original function.
185
+
186
+ ```python
187
+ @extension(to=int)
188
+ def double(x: int) -> int:
189
+ return x * 2
190
+ ```
191
+
192
+ Hovering `double` may not show the expected signature `(x: int) -> int`, but instead `(function) double: ExtensionDecoratorFactory[int]`.
193
+
194
+ ### Uses the `|` operator (`__ror__`)
195
+
196
+ The system works by overriding the right-side bitwise OR operator.
197
+
198
+ ```python
199
+ result = value | extension_call()
200
+ ```
201
+
202
+ This only works if the left-hand type does not fully consume the `|` operator itself.
203
+
204
+ For example, sets already use `|`:
205
+
206
+ ```python
207
+ {1, 2} | {3} # set union
208
+ ```
209
+
210
+ If a type defines its own `__or__` in a way that prevents fallback to `__ror__`, the extension method will not run.
211
+
212
+ ## License
213
+
214
+ This project is licensed under the MIT License. See the `LICENSE` file for details.
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "extensionmethods"
3
+ dynamic = ["version"]
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Pim Mostert", email = "pim.mostert@pimmostert.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "Operating System :: OS Independent",
13
+ ]
14
+ license = "MIT"
15
+ license-files = ["LICEN[CS]E*"]
16
+ dependencies = []
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/Pim-Mostert/extensionmethods"
20
+ Issues = "https://github.com/Pim-Mostert/extensionmethods/issues"
21
+
22
+ [build-system]
23
+ requires = ["setuptools", "wheel", "setuptools-scm"]
24
+ build-backend = "setuptools.build_meta"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "pre-commit>=4.5.1",
29
+ "pytest>=9.0.2",
30
+ ]
31
+
32
+ [tool.setuptools_scm]
33
+ tag_regex = 'v(\d+\.\d+\.\d+)$' # vX.Y.Z (major, minor, patch)
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from .extensions import extension
4
+
5
+ try:
6
+ __version__ = version("extensionmethods")
7
+ except PackageNotFoundError:
8
+ __version__ = "noinstall"
9
+
10
+ __all__ = [
11
+ "extension",
12
+ ]
@@ -0,0 +1,44 @@
1
+ from functools import wraps
2
+ from typing import Any, Callable, Generic, Type, TypeVar
3
+
4
+ T = TypeVar("T")
5
+
6
+
7
+ class ExtensionDecorator(Generic[T]):
8
+ def __init__(self, func: Callable[..., Any], args, kwargs, to: Type[T]):
9
+ self._func = func
10
+ self._to = to
11
+ self._args = args
12
+ self._kwargs = kwargs
13
+
14
+ def __call__(self, _: Any) -> Any:
15
+ raise NotImplementedError("Don't call an extension method directly.")
16
+
17
+ def __ror__(self, other: T) -> Any:
18
+ if not isinstance(other, self._to):
19
+ raise TypeError(
20
+ f"Extension '{self._func.__name__}' can only be used on '{self._to.__name__}', "
21
+ f"not '{type(other).__name__}'"
22
+ )
23
+
24
+ return self._func(other, *self._args, **self._kwargs)
25
+
26
+
27
+ class ExtensionDecoratorFactory(Generic[T]):
28
+ def __init__(self, func: Callable[..., Any], to: Type[T]):
29
+ self._func = func
30
+ self._to = to
31
+
32
+ wraps(func)(self)
33
+
34
+ def __call__(self, *args, **kwargs) -> ExtensionDecorator[T]:
35
+ return ExtensionDecorator(self._func, args, kwargs, self._to)
36
+
37
+
38
+ def extension(
39
+ *, to: Type[T]
40
+ ) -> Callable[[Callable[..., Any]], ExtensionDecoratorFactory[T]]:
41
+ def wrapper(func: Callable[..., Any]) -> ExtensionDecoratorFactory[T]:
42
+ return ExtensionDecoratorFactory(func, to)
43
+
44
+ return wrapper
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: extensionmethods
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author-email: Pim Mostert <pim.mostert@pimmostert.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Pim-Mostert/extensionmethods
8
+ Project-URL: Issues, https://github.com/Pim-Mostert/extensionmethods/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # extensionmethods
17
+
18
+ Mimics C#-style extension methods in Python.
19
+
20
+ `extensionmethods` is a tiny package that lets you “attach” functions to existing types without modifying their source code, enabling method-like syntax, better chaining, and cleaner separation of optional dependencies.
21
+
22
+ - [extensionmethods](#extensionmethods)
23
+ - [Example usage](#example-usage)
24
+ - [Type safety and type checking](#type-safety-and-type-checking)
25
+ - [Installation](#installation)
26
+ - [Why use extension methods?](#why-use-extension-methods)
27
+ - [Readability through chaining](#readability-through-chaining)
28
+ - [Modularity and dependency isolation](#modularity-and-dependency-isolation)
29
+ - [Known caveats](#known-caveats)
30
+ - [IDE type hints may be misleading](#ide-type-hints-may-be-misleading)
31
+ - [Uses the `|` operator (`__ror__`)](#uses-the--operator-__ror__)
32
+ - [License](#license)
33
+
34
+
35
+ ## Example usage
36
+
37
+ In C#, you can add methods to existing types:
38
+
39
+ ```csharp
40
+ public static class IntExtensions
41
+ {
42
+ public static int Double(this int x)
43
+ {
44
+ return x * 2;
45
+ }
46
+ }
47
+
48
+ int result = 7.Double();
49
+ ```
50
+
51
+ You get method syntax without modifying `int`.
52
+
53
+ With the `extensionmethods` package you can achieve similiar functionality like so:
54
+
55
+ ```python
56
+ from extensionmethods import extension
57
+
58
+ @extension(to=int)
59
+ def double(x: int) -> int:
60
+ return x * 2
61
+
62
+ result = 7 | double()
63
+ print(result) # 14
64
+ ```
65
+
66
+ With parameters:
67
+
68
+ ```python
69
+ @extension(to=int)
70
+ def add_then_multiply(x: int, to_add: int, to_multiply: int) -> int:
71
+ return (x + to_add) * to_multiply
72
+
73
+ result = 7 | add_then_multiply(11, 3)
74
+ print(result) # 54
75
+ ```
76
+
77
+ The value on the left side becomes the first argument of the function.
78
+
79
+ ## Type safety and type checking
80
+
81
+ The extension methods are type-aware.
82
+
83
+ When you declare an extension, you bind it to a specific type:
84
+
85
+ ```python
86
+ @extension(to=int)
87
+ def double(x: int) -> int:
88
+ return x * 2
89
+ ```
90
+
91
+ This gives you safety at two levels:
92
+
93
+ - **IDE / static type checking**
94
+ Type checkers and editors can detect incorrect usage:
95
+
96
+ ```python
97
+ "hello" | double() # type error
98
+ ```
99
+
100
+ Your IDE (e.g. VS Code) can flag this because the extension is declared for `int`, not `str`.
101
+
102
+ - **Runtime enforcement**
103
+
104
+ Even if type checking is bypassed, the library validates the type at runtime:
105
+
106
+ ```python
107
+ >>> "hello" | double()
108
+ TypeError: Extension 'double' can only be used on 'int', not 'str'
109
+ ```
110
+
111
+ ## Installation
112
+
113
+ Using **pip**:
114
+
115
+ ```bash
116
+ pip install extensionmethods
117
+ ```
118
+
119
+ Using **uv**:
120
+
121
+ ```bash
122
+ uv pip install extensionmethods
123
+ ```
124
+
125
+ ## Why use extension methods?
126
+
127
+ ### Readability through chaining
128
+
129
+ Instead of nested calls:
130
+
131
+ ```python
132
+ result = normalize(scale(center(data)))
133
+ ```
134
+
135
+ You can express the same flow step-by-step:
136
+
137
+ ```python
138
+ result = data | center() | scale() | normalize()
139
+ ```
140
+
141
+ This reads left-to-right and mirrors how data is conceptually transformed.
142
+
143
+ ### Modularity and dependency isolation
144
+
145
+ Suppose you maintain a core class:
146
+
147
+ ```python
148
+ class Dataset:
149
+ ...
150
+ ```
151
+
152
+ You want export helpers:
153
+
154
+ - `to_pandas()`
155
+ - `to_numpy()`
156
+ - `to_torch()`
157
+
158
+ If you put these methods directly on `Dataset`, your core package must depend on `pandas`, `numpy`, and `torch`.
159
+
160
+ Instead, keep the core dependency-free:
161
+
162
+ ```python
163
+ # core package
164
+ class Dataset:
165
+ ...
166
+ ```
167
+
168
+ Then provide optional extensions:
169
+
170
+ ```python
171
+ # dataset_pandas package
172
+ import pandas as pd
173
+ from extensionmethods import extension
174
+ from core import Dataset
175
+
176
+ @extension(to=Dataset)
177
+ def to_pandas(ds: Dataset) -> pd.DataFrame:
178
+ ...
179
+ ```
180
+
181
+ Usage:
182
+
183
+ ```python
184
+ import dataset_pandas # registers the extension
185
+
186
+ df = dataset | to_pandas()
187
+ ```
188
+
189
+ Now:
190
+
191
+ - The core package has zero heavy dependencies
192
+ - Users only install what they need
193
+ - Functionality stays logically grouped
194
+
195
+ ## Known caveats
196
+
197
+ ### IDE type hints may be misleading
198
+
199
+ Editors like VS Code may show hover/type information for the decorator wrapper, not the original function.
200
+
201
+ ```python
202
+ @extension(to=int)
203
+ def double(x: int) -> int:
204
+ return x * 2
205
+ ```
206
+
207
+ Hovering `double` may not show the expected signature `(x: int) -> int`, but instead `(function) double: ExtensionDecoratorFactory[int]`.
208
+
209
+ ### Uses the `|` operator (`__ror__`)
210
+
211
+ The system works by overriding the right-side bitwise OR operator.
212
+
213
+ ```python
214
+ result = value | extension_call()
215
+ ```
216
+
217
+ This only works if the left-hand type does not fully consume the `|` operator itself.
218
+
219
+ For example, sets already use `|`:
220
+
221
+ ```python
222
+ {1, 2} | {3} # set union
223
+ ```
224
+
225
+ If a type defines its own `__or__` in a way that prevents fallback to `__ror__`, the extension method will not run.
226
+
227
+ ## License
228
+
229
+ This project is licensed under the MIT License. See the `LICENSE` file for details.
@@ -0,0 +1,17 @@
1
+ .gitignore
2
+ .pre-commit-config.yaml
3
+ .python-version
4
+ LICENSE
5
+ README.md
6
+ pyproject.toml
7
+ uv.lock
8
+ .github/workflows/build-test-publish.yml
9
+ .vscode/extensions.json
10
+ .vscode/settings.json
11
+ src/extensionmethods/__init__.py
12
+ src/extensionmethods/extensions.py
13
+ src/extensionmethods.egg-info/PKG-INFO
14
+ src/extensionmethods.egg-info/SOURCES.txt
15
+ src/extensionmethods.egg-info/dependency_links.txt
16
+ src/extensionmethods.egg-info/top_level.txt
17
+ tests/test_extensions.py
@@ -0,0 +1 @@
1
+ extensionmethods
@@ -0,0 +1,53 @@
1
+ import pytest
2
+
3
+ from extensionmethods import extension
4
+
5
+
6
+ def test_extension():
7
+ # Assign
8
+ @extension(to=int)
9
+ def double(x: int) -> int:
10
+ return x * 2
11
+
12
+ # Act
13
+ output = 7 | double()
14
+
15
+ # Assert
16
+ assert output == 7 * 2
17
+
18
+
19
+ def test_extension_single_parameter():
20
+ # Assign
21
+ @extension(to=int)
22
+ def add(x: int, to_add: int) -> int:
23
+ return x + to_add
24
+
25
+ # Act
26
+ output = 7 | add(11)
27
+
28
+ # Assert
29
+ assert 7 + 11 == output
30
+
31
+
32
+ def test_extension_two_parameters():
33
+ # Assign
34
+ @extension(to=int)
35
+ def add_then_multiply(x: int, to_add: int, to_multiply: int) -> int:
36
+ return (x + to_add) * to_multiply
37
+
38
+ # Act
39
+ output = 7 | add_then_multiply(11, 3)
40
+
41
+ # Assert
42
+ assert (7 + 11) * 3 == output
43
+
44
+
45
+ def test_extension_wrong_type():
46
+ # Assign
47
+ @extension(to=str)
48
+ def upper(x: str) -> str:
49
+ return x.upper()
50
+
51
+ # Act
52
+ with pytest.raises(TypeError):
53
+ _ = 1 | upper() # pyright: ignore[reportOperatorIssue]
@@ -0,0 +1,221 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.11"
4
+
5
+ [[package]]
6
+ name = "cfgv"
7
+ version = "3.5.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "colorama"
16
+ version = "0.4.6"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "distlib"
25
+ version = "0.4.0"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
30
+ ]
31
+
32
+ [[package]]
33
+ name = "extensionmethods"
34
+ source = { editable = "." }
35
+
36
+ [package.dev-dependencies]
37
+ dev = [
38
+ { name = "pre-commit" },
39
+ { name = "pytest" },
40
+ ]
41
+
42
+ [package.metadata]
43
+
44
+ [package.metadata.requires-dev]
45
+ dev = [
46
+ { name = "pre-commit", specifier = ">=4.5.1" },
47
+ { name = "pytest", specifier = ">=9.0.2" },
48
+ ]
49
+
50
+ [[package]]
51
+ name = "filelock"
52
+ version = "3.20.3"
53
+ source = { registry = "https://pypi.org/simple" }
54
+ sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
55
+ wheels = [
56
+ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
57
+ ]
58
+
59
+ [[package]]
60
+ name = "identify"
61
+ version = "2.6.16"
62
+ source = { registry = "https://pypi.org/simple" }
63
+ sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" }
64
+ wheels = [
65
+ { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" },
66
+ ]
67
+
68
+ [[package]]
69
+ name = "iniconfig"
70
+ version = "2.3.0"
71
+ source = { registry = "https://pypi.org/simple" }
72
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
73
+ wheels = [
74
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
75
+ ]
76
+
77
+ [[package]]
78
+ name = "nodeenv"
79
+ version = "1.10.0"
80
+ source = { registry = "https://pypi.org/simple" }
81
+ sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
82
+ wheels = [
83
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
84
+ ]
85
+
86
+ [[package]]
87
+ name = "packaging"
88
+ version = "26.0"
89
+ source = { registry = "https://pypi.org/simple" }
90
+ sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
91
+ wheels = [
92
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
93
+ ]
94
+
95
+ [[package]]
96
+ name = "platformdirs"
97
+ version = "4.5.1"
98
+ source = { registry = "https://pypi.org/simple" }
99
+ sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
100
+ wheels = [
101
+ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
102
+ ]
103
+
104
+ [[package]]
105
+ name = "pluggy"
106
+ version = "1.6.0"
107
+ source = { registry = "https://pypi.org/simple" }
108
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
109
+ wheels = [
110
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
111
+ ]
112
+
113
+ [[package]]
114
+ name = "pre-commit"
115
+ version = "4.5.1"
116
+ source = { registry = "https://pypi.org/simple" }
117
+ dependencies = [
118
+ { name = "cfgv" },
119
+ { name = "identify" },
120
+ { name = "nodeenv" },
121
+ { name = "pyyaml" },
122
+ { name = "virtualenv" },
123
+ ]
124
+ sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
125
+ wheels = [
126
+ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
127
+ ]
128
+
129
+ [[package]]
130
+ name = "pygments"
131
+ version = "2.19.2"
132
+ source = { registry = "https://pypi.org/simple" }
133
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
134
+ wheels = [
135
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
136
+ ]
137
+
138
+ [[package]]
139
+ name = "pytest"
140
+ version = "9.0.2"
141
+ source = { registry = "https://pypi.org/simple" }
142
+ dependencies = [
143
+ { name = "colorama", marker = "sys_platform == 'win32'" },
144
+ { name = "iniconfig" },
145
+ { name = "packaging" },
146
+ { name = "pluggy" },
147
+ { name = "pygments" },
148
+ ]
149
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
150
+ wheels = [
151
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
152
+ ]
153
+
154
+ [[package]]
155
+ name = "pyyaml"
156
+ version = "6.0.3"
157
+ source = { registry = "https://pypi.org/simple" }
158
+ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
159
+ wheels = [
160
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
161
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
162
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
163
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
164
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
165
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
166
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
167
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
168
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
169
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
170
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
171
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
172
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
173
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
174
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
175
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
176
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
177
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
178
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
179
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
180
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
181
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
182
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
183
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
184
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
185
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
186
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
187
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
188
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
189
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
190
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
191
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
192
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
193
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
194
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
195
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
196
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
197
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
198
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
199
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
200
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
201
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
202
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
203
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
204
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
205
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
206
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
207
+ ]
208
+
209
+ [[package]]
210
+ name = "virtualenv"
211
+ version = "20.36.1"
212
+ source = { registry = "https://pypi.org/simple" }
213
+ dependencies = [
214
+ { name = "distlib" },
215
+ { name = "filelock" },
216
+ { name = "platformdirs" },
217
+ ]
218
+ sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" }
219
+ wheels = [
220
+ { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" },
221
+ ]