pysqlscribe 0.0.1__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,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # IDE
13
+ .idea
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Max Pumperla
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
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: pysqlscribe
3
+ Version: 0.0.1
4
+ Summary: A simple Python Library for building relational database queries using objects
5
+ Project-URL: Source code, https://github.com/danielenricocahall/pysqlscribe
6
+ Author-email: Daniel Cahall <danielenricocahall@gmail.com>
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Overview
12
+ [![Supported Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue)](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-blue)
13
+
14
+ This is `pysqlscribe`, the Python library intended to make building SQL queries in your code a bit easier!
15
+
16
+
17
+ # Motivation
18
+ Other query building libraries, such as [pypika](https://github.com/kayak/pypika) are fantastic but not actively maintained. Some ORM libraries such as `sqlalchemy` offer similar (and awesome) capabilities using the core API, but if you're not already using the library in your application, it's a bit of a large dependency to introduce for the purposes of query building.
19
+
20
+
21
+ # API
22
+
23
+ `pysqlscribe` currently offers several APIs for building queries.
24
+
25
+ ## Query
26
+
27
+ A `Query` object can be constructed using the `QueryRegistry`'s `get_builder` if you supply a valid dialect (e.g; "mysql", "postgres", "oracle). For example, "mysql" would be:
28
+
29
+ ```python
30
+ from pysqlscribe.query import QueryRegistry
31
+
32
+ query_builder = QueryRegistry.get_builder("mysql")
33
+ query = query_builder.select("test_field", "another_test_field").from_("test_table").build()
34
+ ```
35
+
36
+ Alternatively, you can create the corresponding `Query` class associated with the dialect directly:
37
+
38
+ ```python
39
+ from pysqlscribe.query import MySQLQuery
40
+
41
+ query_builder = MySQLQuery()
42
+ query = query_builder.select("test_field", "another_test_field").from_("test_table").build()
43
+ ```
44
+
45
+
46
+ Furthermore, if there are any dialects that we currently don't support, you can create your own by subclassing `Query` and registering it with the `QueryRegistry`:
47
+
48
+ ```python
49
+ from pysqlscribe.query import QueryRegistry, Query
50
+
51
+
52
+ @QueryRegistry.register("custom")
53
+ class CustomQuery(Query):
54
+ ...
55
+ ```
56
+
57
+ ## Table
58
+ An alternative method for building queries is through the `Table` object:
59
+
60
+ ```python
61
+ from pysqlscribe.table import MySQLTable
62
+
63
+ table = MySQLTable("test_table", "test_field", "another_test_field")
64
+ query = table.select("test_field").build()
65
+ ```
66
+
67
+ A schema for the table can also be provided as a keyword argument, after the columns/fields:
68
+
69
+ ```python
70
+ from pysqlscribe.table import MySQLTable
71
+
72
+ table = MySQLTable("test_table", "test_field", "another_test_field", schema="test_schema")
73
+ query = table.select("test_field").build()
74
+ ```
75
+
76
+ Additionally, in the event an invalid field is provided in the `select` call, we will raise an exception:
77
+
78
+ ```python
79
+ from pysqlscribe.table import MySQLTable
80
+
81
+ table = MySQLTable("test_table", "test_field", "another_test_field")
82
+ table.select("some_nonexistent_field") # will raise InvalidFieldsException
83
+ ```
84
+
85
+ `Table` also offers a `create` method in the event you've added a new dialect which doesn't have an associated `Table` implementation, or if you need to change it for different environments (e.g; `sqlite` for local development, `mysql`/`postgres`/`oracle`/etc. for deployment):
86
+
87
+ ```python
88
+ from pysqlscribe.table import Table
89
+
90
+ new_dialect_table_class = Table.create(
91
+ "new-dialect") # assuming you've registered "new-dialect" with the `QueryRegistry`
92
+ table = new_dialect_table_class("test_table", "test_field", "another_test_field")
93
+ ```
94
+
95
+ You can overwrite the original fields supplied to a `Table` as well, which will delete the old attributes and set new ones:
96
+
97
+ ```python
98
+ from pysqlscribe.table import MySQLTable
99
+
100
+ table = MySQLTable("test_table", "test_field", "another_test_field")
101
+ table.test_field # valid
102
+ table.fields = ['new_test_field']
103
+ table.select("new_test_field")
104
+ table.new_test_field # now valid - but `table.test_field` is not anymore
105
+ ```
106
+
107
+
108
+
109
+
110
+ ## Schema
111
+ For associating multiple `Table`s with a single schema, you can use the `Schema`:
112
+
113
+ ```python
114
+ from pysqlscribe.schema import Schema
115
+
116
+ schema = Schema("test_schema", tables=["test_table", "another_test_table"], dialect="postgres")
117
+ schema.tables # a list of two `Table` objects
118
+ ```
119
+
120
+ This is functionally equivalent to:
121
+
122
+ ```python
123
+ from pysqlscribe.table import PostgresTable
124
+
125
+ table = PostgresTable("test_table", schema="test_schema")
126
+ another_table = PostgresTable("another_test_table", schema="test_schema")
127
+ ```
128
+
129
+ Instead of supplying a `dialect` directly to `Schema`, you can also set the environment variable `PYQUERY_BUILDER_DIALECT`:
130
+
131
+ ```shell
132
+ export PYQUERY_BUILDER_DIALECT = 'postgres'
133
+ ```
134
+
135
+ ```python
136
+ from pysqlscribe.schema import Schema
137
+
138
+ schema = Schema("test_schema", tables=["test_table", "another_test_table"])
139
+ schema.tables # a list of two `PostgresTable` objects
140
+ ```
141
+
142
+ Alternatively, if you already have existing `Table` objects you want to associate with the schema, you can supply them directly (in this case, `dialect` is not needed):
143
+
144
+ ```python
145
+ from pysqlscribe.schema import Schema
146
+ from pysqlscribe.table import PostgresTable
147
+
148
+ table = PostgresTable("test_table")
149
+ another_table = PostgresTable("another_test_table")
150
+ schema = Schema("test_schema", [table, another_table])
151
+ ```
152
+
153
+
154
+ `Schema` also has each table set as an attribute, so in the example above, you can do the following:
155
+
156
+ ```python
157
+ schema.test_table # will return the supplied table object with the name `"test_table"`
158
+ ```
159
+
160
+
161
+ # Contributions
162
+
163
+ Contributions are always welcome, as this is a new and active project.
164
+
165
+ ## Local Environment Setup
166
+ This project currently uses `uv` for convenience, although we currently only have dev dependencies. To create your environment:
167
+ ```shell
168
+ uv sync
169
+ ```
170
+
171
+ ## Unit Testing
172
+ `pytest` is used for all unit testing. To run the unit tests locally (assuming a local environment is set up):
173
+ ```shell
174
+ uv run pytest
175
+ ```
176
+
177
+ Unit tests are executed as part of CI, and the behavior should be consistent with what is observed in local development.
178
+
179
+
180
+ # Supported Dialects
181
+ This is anticipated to grow, also there are certainly operations that are missing within dialects.
182
+ - [X] `MySQL`
183
+ - [X] `Oracle`
184
+ - [X] `Postgres`
185
+ - [X] `Sqlite`
186
+
187
+
188
+ # TODO
189
+ - [ ] Support `JOIN`s
190
+ - [ ] Add more dialects
191
+ - [ ] Support `OFFSET` for Oracle and SQLServer
192
+ - [ ] Add a better abstraction around fields such that we can build expressions from comparisons, etc. as strings are limiting
@@ -0,0 +1,182 @@
1
+ # Overview
2
+ [![Supported Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue)](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-blue)
3
+
4
+ This is `pysqlscribe`, the Python library intended to make building SQL queries in your code a bit easier!
5
+
6
+
7
+ # Motivation
8
+ Other query building libraries, such as [pypika](https://github.com/kayak/pypika) are fantastic but not actively maintained. Some ORM libraries such as `sqlalchemy` offer similar (and awesome) capabilities using the core API, but if you're not already using the library in your application, it's a bit of a large dependency to introduce for the purposes of query building.
9
+
10
+
11
+ # API
12
+
13
+ `pysqlscribe` currently offers several APIs for building queries.
14
+
15
+ ## Query
16
+
17
+ A `Query` object can be constructed using the `QueryRegistry`'s `get_builder` if you supply a valid dialect (e.g; "mysql", "postgres", "oracle). For example, "mysql" would be:
18
+
19
+ ```python
20
+ from pysqlscribe.query import QueryRegistry
21
+
22
+ query_builder = QueryRegistry.get_builder("mysql")
23
+ query = query_builder.select("test_field", "another_test_field").from_("test_table").build()
24
+ ```
25
+
26
+ Alternatively, you can create the corresponding `Query` class associated with the dialect directly:
27
+
28
+ ```python
29
+ from pysqlscribe.query import MySQLQuery
30
+
31
+ query_builder = MySQLQuery()
32
+ query = query_builder.select("test_field", "another_test_field").from_("test_table").build()
33
+ ```
34
+
35
+
36
+ Furthermore, if there are any dialects that we currently don't support, you can create your own by subclassing `Query` and registering it with the `QueryRegistry`:
37
+
38
+ ```python
39
+ from pysqlscribe.query import QueryRegistry, Query
40
+
41
+
42
+ @QueryRegistry.register("custom")
43
+ class CustomQuery(Query):
44
+ ...
45
+ ```
46
+
47
+ ## Table
48
+ An alternative method for building queries is through the `Table` object:
49
+
50
+ ```python
51
+ from pysqlscribe.table import MySQLTable
52
+
53
+ table = MySQLTable("test_table", "test_field", "another_test_field")
54
+ query = table.select("test_field").build()
55
+ ```
56
+
57
+ A schema for the table can also be provided as a keyword argument, after the columns/fields:
58
+
59
+ ```python
60
+ from pysqlscribe.table import MySQLTable
61
+
62
+ table = MySQLTable("test_table", "test_field", "another_test_field", schema="test_schema")
63
+ query = table.select("test_field").build()
64
+ ```
65
+
66
+ Additionally, in the event an invalid field is provided in the `select` call, we will raise an exception:
67
+
68
+ ```python
69
+ from pysqlscribe.table import MySQLTable
70
+
71
+ table = MySQLTable("test_table", "test_field", "another_test_field")
72
+ table.select("some_nonexistent_field") # will raise InvalidFieldsException
73
+ ```
74
+
75
+ `Table` also offers a `create` method in the event you've added a new dialect which doesn't have an associated `Table` implementation, or if you need to change it for different environments (e.g; `sqlite` for local development, `mysql`/`postgres`/`oracle`/etc. for deployment):
76
+
77
+ ```python
78
+ from pysqlscribe.table import Table
79
+
80
+ new_dialect_table_class = Table.create(
81
+ "new-dialect") # assuming you've registered "new-dialect" with the `QueryRegistry`
82
+ table = new_dialect_table_class("test_table", "test_field", "another_test_field")
83
+ ```
84
+
85
+ You can overwrite the original fields supplied to a `Table` as well, which will delete the old attributes and set new ones:
86
+
87
+ ```python
88
+ from pysqlscribe.table import MySQLTable
89
+
90
+ table = MySQLTable("test_table", "test_field", "another_test_field")
91
+ table.test_field # valid
92
+ table.fields = ['new_test_field']
93
+ table.select("new_test_field")
94
+ table.new_test_field # now valid - but `table.test_field` is not anymore
95
+ ```
96
+
97
+
98
+
99
+
100
+ ## Schema
101
+ For associating multiple `Table`s with a single schema, you can use the `Schema`:
102
+
103
+ ```python
104
+ from pysqlscribe.schema import Schema
105
+
106
+ schema = Schema("test_schema", tables=["test_table", "another_test_table"], dialect="postgres")
107
+ schema.tables # a list of two `Table` objects
108
+ ```
109
+
110
+ This is functionally equivalent to:
111
+
112
+ ```python
113
+ from pysqlscribe.table import PostgresTable
114
+
115
+ table = PostgresTable("test_table", schema="test_schema")
116
+ another_table = PostgresTable("another_test_table", schema="test_schema")
117
+ ```
118
+
119
+ Instead of supplying a `dialect` directly to `Schema`, you can also set the environment variable `PYQUERY_BUILDER_DIALECT`:
120
+
121
+ ```shell
122
+ export PYQUERY_BUILDER_DIALECT = 'postgres'
123
+ ```
124
+
125
+ ```python
126
+ from pysqlscribe.schema import Schema
127
+
128
+ schema = Schema("test_schema", tables=["test_table", "another_test_table"])
129
+ schema.tables # a list of two `PostgresTable` objects
130
+ ```
131
+
132
+ Alternatively, if you already have existing `Table` objects you want to associate with the schema, you can supply them directly (in this case, `dialect` is not needed):
133
+
134
+ ```python
135
+ from pysqlscribe.schema import Schema
136
+ from pysqlscribe.table import PostgresTable
137
+
138
+ table = PostgresTable("test_table")
139
+ another_table = PostgresTable("another_test_table")
140
+ schema = Schema("test_schema", [table, another_table])
141
+ ```
142
+
143
+
144
+ `Schema` also has each table set as an attribute, so in the example above, you can do the following:
145
+
146
+ ```python
147
+ schema.test_table # will return the supplied table object with the name `"test_table"`
148
+ ```
149
+
150
+
151
+ # Contributions
152
+
153
+ Contributions are always welcome, as this is a new and active project.
154
+
155
+ ## Local Environment Setup
156
+ This project currently uses `uv` for convenience, although we currently only have dev dependencies. To create your environment:
157
+ ```shell
158
+ uv sync
159
+ ```
160
+
161
+ ## Unit Testing
162
+ `pytest` is used for all unit testing. To run the unit tests locally (assuming a local environment is set up):
163
+ ```shell
164
+ uv run pytest
165
+ ```
166
+
167
+ Unit tests are executed as part of CI, and the behavior should be consistent with what is observed in local development.
168
+
169
+
170
+ # Supported Dialects
171
+ This is anticipated to grow, also there are certainly operations that are missing within dialects.
172
+ - [X] `MySQL`
173
+ - [X] `Oracle`
174
+ - [X] `Postgres`
175
+ - [X] `Sqlite`
176
+
177
+
178
+ # TODO
179
+ - [ ] Support `JOIN`s
180
+ - [ ] Add more dialects
181
+ - [ ] Support `OFFSET` for Oracle and SQLServer
182
+ - [ ] Add a better abstraction around fields such that we can build expressions from comparisons, etc. as strings are limiting
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "pysqlscribe"
3
+ version = "0.0.1"
4
+ description = "A simple Python Library for building relational database queries using objects"
5
+ readme = "README.md"
6
+ authors = [{ name="Daniel Cahall", email="danielenricocahall@gmail.com" }]
7
+ requires-python = ">=3.10"
8
+ dependencies = []
9
+ license-files = ["LICENSE"]
10
+
11
+ [project.urls]
12
+ "Source code" = "https://github.com/danielenricocahall/pysqlscribe"
13
+
14
+ [tool.uv]
15
+
16
+ [dependency-groups]
17
+ dev = [
18
+ "pre-commit>=4.1.0",
19
+ "pytest>=8.3.4",
20
+ "ruff>=0.9.2",
21
+ ]
22
+
23
+
24
+ [build-system]
25
+ requires = ["hatchling"]
26
+ build-backend = "hatchling.build"
27
+
28
+ [tool.hatch.build]
29
+ exclude = [
30
+ "/.*",
31
+ "/tests",
32
+ "README.md",
33
+
34
+ ]
File without changes
@@ -0,0 +1,249 @@
1
+ import operator
2
+ from abc import abstractmethod, ABC
3
+ from functools import reduce
4
+ from typing import Any, Dict, Self, Callable, Tuple
5
+
6
+ from pysqlscribe.regex_patterns import VALID_IDENTIFIER_REGEX
7
+
8
+ SELECT = "SELECT"
9
+ FROM = "FROM"
10
+ WHERE = "WHERE"
11
+ LIMIT = "LIMIT"
12
+ ORDER_BY = "ORDER BY"
13
+ AND = "AND"
14
+ FETCH_NEXT = "FETCH NEXT"
15
+ OFFSET = "OFFSET"
16
+
17
+
18
+ def reconcile_args_into_string(*args, escape_identifier: Callable[[str], str]) -> str:
19
+ arg = args[0]
20
+ if isinstance(arg, str):
21
+ arg = [arg]
22
+ for identifier in arg:
23
+ identifier = identifier.strip()
24
+ if not VALID_IDENTIFIER_REGEX.match(identifier):
25
+ raise ValueError(f"Invalid SQL identifier: {identifier}")
26
+ identifiers = [escape_identifier(identifier) for identifier in arg]
27
+ return ",".join(identifiers)
28
+
29
+
30
+ class InvalidNodeException(Exception): ...
31
+
32
+
33
+ class Node(ABC):
34
+ next_: Self | None = None
35
+ prev_: Self | None = None
36
+ state: dict[str, Any]
37
+
38
+ def __init__(self, state):
39
+ self.state = state
40
+
41
+ def add(self, next_: "Node"):
42
+ if not isinstance(next_, self.valid_next_nodes):
43
+ raise InvalidNodeException()
44
+ next_.prev_ = self
45
+ self.next_ = next_
46
+
47
+ @property
48
+ @abstractmethod
49
+ def valid_next_nodes(self) -> Tuple[type[Self], ...]: ...
50
+
51
+
52
+ class SelectNode(Node):
53
+ @property
54
+ def valid_next_nodes(self):
55
+ return (FromNode,)
56
+
57
+ def __str__(self):
58
+ return f"{SELECT} {self.state['fields']}"
59
+
60
+
61
+ class FromNode(Node):
62
+ @property
63
+ def valid_next_nodes(self):
64
+ return WhereNode, GroupByNode, OrderByNode, LimitNode
65
+
66
+ def __str__(self):
67
+ return f"{FROM} {self.state['tables']}"
68
+
69
+
70
+ class WhereNode(Node):
71
+ @property
72
+ def valid_next_nodes(self):
73
+ return GroupByNode, OrderByNode, LimitNode, WhereNode
74
+
75
+ def __str__(self):
76
+ return f"{WHERE} {self.state['conditions']}"
77
+
78
+ def __and__(self, other):
79
+ if isinstance(other, WhereNode):
80
+ compound_condition = (
81
+ f"{self.state['conditions']} {AND} {other.state['conditions']}"
82
+ )
83
+ return WhereNode({"conditions": compound_condition})
84
+
85
+
86
+ class GroupByNode(Node):
87
+ @property
88
+ def valid_next_nodes(self):
89
+ return OrderByNode
90
+
91
+
92
+ class OrderByNode(Node):
93
+ @property
94
+ def valid_next_nodes(self):
95
+ return LimitNode
96
+
97
+ def __str__(self):
98
+ return f"{ORDER_BY} {self.state['fields']}"
99
+
100
+
101
+ class LimitNode(Node):
102
+ @property
103
+ def valid_next_nodes(self):
104
+ return OffsetNode
105
+
106
+ def __str__(self):
107
+ return f"{LIMIT} {self.state['limit']}"
108
+
109
+
110
+ class FetchNextNode(LimitNode):
111
+ @property
112
+ def valid_next_nodes(self):
113
+ return ()
114
+
115
+ def __str__(self):
116
+ return f"{FETCH_NEXT} {self.state['limit']} ROWS ONLY"
117
+
118
+
119
+ class OffsetNode(Node):
120
+ @property
121
+ def valid_next_nodes(self):
122
+ return ()
123
+
124
+ def __str__(self):
125
+ return f"{OFFSET} {self.state['offset']}"
126
+
127
+
128
+ class Query(ABC):
129
+ node: Node | None = None
130
+
131
+ def select(self, *args) -> Self:
132
+ if not self.node:
133
+ self.node = SelectNode(
134
+ {
135
+ "fields": reconcile_args_into_string(
136
+ args, escape_identifier=self.escape_identifier
137
+ )
138
+ }
139
+ )
140
+ return self
141
+
142
+ def from_(self, *args) -> Self:
143
+ self.node.add(
144
+ FromNode(
145
+ {
146
+ "tables": reconcile_args_into_string(
147
+ args, escape_identifier=self.escape_identifier
148
+ )
149
+ }
150
+ )
151
+ )
152
+ self.node = self.node.next_
153
+ return self
154
+
155
+ def where(self, *args) -> Self:
156
+ where_node = reduce(
157
+ operator.and_, map(lambda arg: WhereNode({"conditions": arg}), args)
158
+ )
159
+ self.node.add(where_node)
160
+ self.node = self.node.next_
161
+ return self
162
+
163
+ def order_by(self, *args) -> Self:
164
+ self.node.add(
165
+ OrderByNode(
166
+ {
167
+ "fields": reconcile_args_into_string(
168
+ args, escape_identifier=self.escape_identifier
169
+ )
170
+ }
171
+ )
172
+ )
173
+ self.node = self.node.next_
174
+ return self
175
+
176
+ def limit(self, n: int | str):
177
+ self.node.add(LimitNode({"limit": int(n)}))
178
+ self.node = self.node = self.node.next_
179
+ return self
180
+
181
+ def offset(self, n: int | str):
182
+ self.node.add(OffsetNode({"offset": int(n)}))
183
+ self.node = self.node = self.node.next_
184
+ return self
185
+
186
+ def build(self, clear: bool = True) -> str:
187
+ node = self.node
188
+ query = ""
189
+ while True:
190
+ query = str(node) + " " + query
191
+ node = node.prev_
192
+ if node is None:
193
+ break
194
+ if clear:
195
+ # we provide an option to not clear the builder in the event the developer needs
196
+ # to debug or needs to reuse the value. By default, we do immediately after building the query
197
+ self.node = None
198
+ return query.strip()
199
+
200
+ def __str__(self):
201
+ return self.build(clear=False)
202
+
203
+ @abstractmethod
204
+ def escape_identifier(self, identifier: str): ...
205
+
206
+
207
+ class QueryRegistry:
208
+ builders: Dict[str, Query] = {}
209
+
210
+ @classmethod
211
+ def register(cls, key: str):
212
+ def decorator(builder_class: Callable[[], Query]) -> Callable[[], Query]:
213
+ cls.builders[key] = builder_class()
214
+ return builder_class
215
+
216
+ return decorator
217
+
218
+ @classmethod
219
+ def get_builder(cls, key: str) -> Query:
220
+ return cls.builders[key]
221
+
222
+
223
+ @QueryRegistry.register("mysql")
224
+ class MySQLQuery(Query):
225
+ def escape_identifier(self, identifier: str) -> str:
226
+ return f"`{identifier}`"
227
+
228
+
229
+ @QueryRegistry.register("oracle")
230
+ class OracleQuery(Query):
231
+ def escape_identifier(self, identifier: str) -> str:
232
+ return f'"{identifier}"'
233
+
234
+ def limit(self, n: int | str):
235
+ self.node.add(FetchNextNode({"limit": int(n)}))
236
+ self.node = self.node = self.node.next_
237
+ return self
238
+
239
+
240
+ @QueryRegistry.register("postgres")
241
+ class PostgreSQLQuery(Query):
242
+ def escape_identifier(self, identifier: str) -> str:
243
+ return f'"{identifier}"'
244
+
245
+
246
+ @QueryRegistry.register("sqlite")
247
+ class SQLiteQuery(Query):
248
+ def escape_identifier(self, identifier: str) -> str:
249
+ return f'"{identifier}"'
@@ -0,0 +1,5 @@
1
+ import re
2
+
3
+ VALID_IDENTIFIER_REGEX = re.compile(
4
+ r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$"
5
+ )
@@ -0,0 +1,60 @@
1
+ import os
2
+ from typing import List
3
+
4
+ from pysqlscribe.regex_patterns import VALID_IDENTIFIER_REGEX
5
+ from pysqlscribe.table import Table
6
+
7
+
8
+ class InvalidSchemaNameException(Exception): ...
9
+
10
+
11
+ class DialectNotSpecifiedException(Exception): ...
12
+
13
+
14
+ class Schema:
15
+ def __init__(
16
+ self,
17
+ name: str,
18
+ tables: List[Table | str] | None = None,
19
+ dialect: str | None = None,
20
+ ):
21
+ self.name = name
22
+ self.dialect = dialect
23
+ self.tables = tables
24
+
25
+ @property
26
+ def name(self):
27
+ return self._name
28
+
29
+ @name.setter
30
+ def name(self, schema_name: str):
31
+ if not VALID_IDENTIFIER_REGEX.match(schema_name):
32
+ raise InvalidSchemaNameException(f"Invalid schema name {schema_name}")
33
+ self._name = schema_name
34
+
35
+ @property
36
+ def tables(self):
37
+ return self._tables
38
+
39
+ @tables.setter
40
+ def tables(self, tables_: list[str | Table]):
41
+ if all(isinstance(table, str) for table in tables_):
42
+ tables_ = [Table.create(self.dialect)(table_name) for table_name in tables_]
43
+ for table in tables_:
44
+ setattr(self, table.name, table)
45
+ table.schema = self.name
46
+ self._tables = tables_
47
+
48
+ @property
49
+ def dialect(self):
50
+ return self._dialect
51
+
52
+ @dialect.setter
53
+ def dialect(self, dialect_: str | None):
54
+ if dialect_:
55
+ self._dialect = dialect_
56
+ elif env_set_dialect := os.environ.get("PYQUERY_BUILDER_DIALECT"):
57
+ self._dialect = env_set_dialect
58
+ else:
59
+ # the user may have provided no `dialect` - this is fine if they're directly supplying `Table` objects.
60
+ self._dialect = dialect_
@@ -0,0 +1,83 @@
1
+ from abc import ABC
2
+ from typing import List
3
+
4
+ from pysqlscribe.query import QueryRegistry
5
+ from pysqlscribe.regex_patterns import VALID_IDENTIFIER_REGEX
6
+
7
+
8
+ class InvalidFieldsException(Exception): ...
9
+
10
+
11
+ class InvalidTableNameException(Exception): ...
12
+
13
+
14
+ class Table(ABC):
15
+ __cache: dict[str, type["Table"]] = {}
16
+
17
+ def __init__(self, name: str, *fields, schema: str | None = None):
18
+ self.name = name
19
+ self.schema = schema
20
+ self.fields = fields
21
+
22
+ @classmethod
23
+ def create(cls, dialect: str):
24
+ if dialect not in QueryRegistry.builders:
25
+ raise ValueError(f"Unsupported dialect: {dialect}")
26
+
27
+ class_name = f"{dialect.capitalize()}Table"
28
+
29
+ if class_name in cls.__cache:
30
+ return cls.__cache[class_name]
31
+
32
+ query_class = QueryRegistry.get_builder(dialect).__class__
33
+
34
+ class DynamicTable(query_class, Table):
35
+ def __init__(self, name: str, *fields, schema: str | None = None):
36
+ query_class.__init__(self)
37
+ Table.__init__(self, name, *fields, schema=schema)
38
+
39
+ def select(self, *fields):
40
+ try:
41
+ assert all(hasattr(self, field) for field in fields)
42
+ return super().select(*fields).from_(self.name)
43
+ except AssertionError:
44
+ raise InvalidFieldsException(
45
+ f"Table {self.name} doesn't have one or more of the fields provided"
46
+ )
47
+
48
+ # Set a meaningful class name
49
+ DynamicTable.__name__ = class_name
50
+ cls.__cache[class_name] = DynamicTable
51
+
52
+ return DynamicTable
53
+
54
+ @property
55
+ def name(self):
56
+ if self.schema:
57
+ return f"{self.schema}.{self._name}"
58
+ return self._name
59
+
60
+ @name.setter
61
+ def name(self, table_name: str):
62
+ if not VALID_IDENTIFIER_REGEX.match(table_name):
63
+ raise InvalidTableNameException(f"Invalid table name {table_name}")
64
+ self._name = table_name
65
+
66
+ @property
67
+ def fields(self):
68
+ return self._fields
69
+
70
+ @fields.setter
71
+ def fields(self, fields_: List[str]):
72
+ if getattr(self, "_fields", None):
73
+ for field in self.fields:
74
+ delattr(self, field)
75
+ self._fields = fields_
76
+ for field in fields_:
77
+ setattr(self, field, None)
78
+
79
+
80
+ MySQLTable = Table.create("mysql")
81
+ OracleTable = Table.create("oracle")
82
+ PostgresTable = Table.create("postgres")
83
+ SqliteTable = Table.create("sqlite")
@@ -0,0 +1,277 @@
1
+ version = 1
2
+ requires-python = ">=3.10"
3
+
4
+ [[package]]
5
+ name = "cfgv"
6
+ version = "3.4.0"
7
+ source = { registry = "https://pypi.org/simple" }
8
+ sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 }
9
+ wheels = [
10
+ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 },
11
+ ]
12
+
13
+ [[package]]
14
+ name = "colorama"
15
+ version = "0.4.6"
16
+ source = { registry = "https://pypi.org/simple" }
17
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
18
+ wheels = [
19
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
20
+ ]
21
+
22
+ [[package]]
23
+ name = "distlib"
24
+ version = "0.3.9"
25
+ source = { registry = "https://pypi.org/simple" }
26
+ sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 }
27
+ wheels = [
28
+ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
29
+ ]
30
+
31
+ [[package]]
32
+ name = "exceptiongroup"
33
+ version = "1.2.2"
34
+ source = { registry = "https://pypi.org/simple" }
35
+ sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
36
+ wheels = [
37
+ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
38
+ ]
39
+
40
+ [[package]]
41
+ name = "filelock"
42
+ version = "3.17.0"
43
+ source = { registry = "https://pypi.org/simple" }
44
+ sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 }
45
+ wheels = [
46
+ { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 },
47
+ ]
48
+
49
+ [[package]]
50
+ name = "identify"
51
+ version = "2.6.6"
52
+ source = { registry = "https://pypi.org/simple" }
53
+ sdist = { url = "https://files.pythonhosted.org/packages/82/bf/c68c46601bacd4c6fb4dd751a42b6e7087240eaabc6487f2ef7a48e0e8fc/identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251", size = 99217 }
54
+ wheels = [
55
+ { url = "https://files.pythonhosted.org/packages/74/a1/68a395c17eeefb04917034bd0a1bfa765e7654fa150cca473d669aa3afb5/identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881", size = 99083 },
56
+ ]
57
+
58
+ [[package]]
59
+ name = "iniconfig"
60
+ version = "2.0.0"
61
+ source = { registry = "https://pypi.org/simple" }
62
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
63
+ wheels = [
64
+ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
65
+ ]
66
+
67
+ [[package]]
68
+ name = "nodeenv"
69
+ version = "1.9.1"
70
+ source = { registry = "https://pypi.org/simple" }
71
+ sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
72
+ wheels = [
73
+ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
74
+ ]
75
+
76
+ [[package]]
77
+ name = "packaging"
78
+ version = "24.2"
79
+ source = { registry = "https://pypi.org/simple" }
80
+ sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
81
+ wheels = [
82
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
83
+ ]
84
+
85
+ [[package]]
86
+ name = "platformdirs"
87
+ version = "4.3.6"
88
+ source = { registry = "https://pypi.org/simple" }
89
+ sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
90
+ wheels = [
91
+ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
92
+ ]
93
+
94
+ [[package]]
95
+ name = "pluggy"
96
+ version = "1.5.0"
97
+ source = { registry = "https://pypi.org/simple" }
98
+ sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
99
+ wheels = [
100
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
101
+ ]
102
+
103
+ [[package]]
104
+ name = "pre-commit"
105
+ version = "4.1.0"
106
+ source = { registry = "https://pypi.org/simple" }
107
+ dependencies = [
108
+ { name = "cfgv" },
109
+ { name = "identify" },
110
+ { name = "nodeenv" },
111
+ { name = "pyyaml" },
112
+ { name = "virtualenv" },
113
+ ]
114
+ sdist = { url = "https://files.pythonhosted.org/packages/2a/13/b62d075317d8686071eb843f0bb1f195eb332f48869d3c31a4c6f1e063ac/pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4", size = 193330 }
115
+ wheels = [
116
+ { url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 },
117
+ ]
118
+
119
+ [[package]]
120
+ name = "pysqlscribe"
121
+ version = "0.0.1"
122
+ source = { editable = "." }
123
+
124
+ [package.dev-dependencies]
125
+ dev = [
126
+ { name = "pre-commit" },
127
+ { name = "pytest" },
128
+ { name = "ruff" },
129
+ ]
130
+
131
+ [package.metadata]
132
+
133
+ [package.metadata.requires-dev]
134
+ dev = [
135
+ { name = "pre-commit", specifier = ">=4.1.0" },
136
+ { name = "pytest", specifier = ">=8.3.4" },
137
+ { name = "ruff", specifier = ">=0.9.2" },
138
+ ]
139
+
140
+ [[package]]
141
+ name = "pytest"
142
+ version = "8.3.4"
143
+ source = { registry = "https://pypi.org/simple" }
144
+ dependencies = [
145
+ { name = "colorama", marker = "sys_platform == 'win32'" },
146
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
147
+ { name = "iniconfig" },
148
+ { name = "packaging" },
149
+ { name = "pluggy" },
150
+ { name = "tomli", marker = "python_full_version < '3.11'" },
151
+ ]
152
+ sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
153
+ wheels = [
154
+ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
155
+ ]
156
+
157
+ [[package]]
158
+ name = "pyyaml"
159
+ version = "6.0.2"
160
+ source = { registry = "https://pypi.org/simple" }
161
+ sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
162
+ wheels = [
163
+ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 },
164
+ { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 },
165
+ { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 },
166
+ { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 },
167
+ { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 },
168
+ { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 },
169
+ { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 },
170
+ { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 },
171
+ { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 },
172
+ { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 },
173
+ { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 },
174
+ { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 },
175
+ { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 },
176
+ { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 },
177
+ { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 },
178
+ { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 },
179
+ { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 },
180
+ { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 },
181
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
182
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
183
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
184
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
185
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
186
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
187
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
188
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
189
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
190
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
191
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
192
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
193
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
194
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
195
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
196
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
197
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
198
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
199
+ ]
200
+
201
+ [[package]]
202
+ name = "ruff"
203
+ version = "0.9.5"
204
+ source = { registry = "https://pypi.org/simple" }
205
+ sdist = { url = "https://files.pythonhosted.org/packages/02/74/6c359f6b9ed85b88df6ef31febce18faeb852f6c9855651dfb1184a46845/ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c", size = 3634177 }
206
+ wheels = [
207
+ { url = "https://files.pythonhosted.org/packages/17/4b/82b7c9ac874e72b82b19fd7eab57d122e2df44d2478d90825854f9232d02/ruff-0.9.5-py3-none-linux_armv6l.whl", hash = "sha256:d466d2abc05f39018d53f681fa1c0ffe9570e6d73cde1b65d23bb557c846f442", size = 11681264 },
208
+ { url = "https://files.pythonhosted.org/packages/27/5c/f5ae0a9564e04108c132e1139d60491c0abc621397fe79a50b3dc0bd704b/ruff-0.9.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38840dbcef63948657fa7605ca363194d2fe8c26ce8f9ae12eee7f098c85ac8a", size = 11657554 },
209
+ { url = "https://files.pythonhosted.org/packages/2a/83/c6926fa3ccb97cdb3c438bb56a490b395770c750bf59f9bc1fe57ae88264/ruff-0.9.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d56ba06da53536b575fbd2b56517f6f95774ff7be0f62c80b9e67430391eeb36", size = 11088959 },
210
+ { url = "https://files.pythonhosted.org/packages/af/a7/42d1832b752fe969ffdbfcb1b4cb477cb271bed5835110fb0a16ef31ab81/ruff-0.9.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7cb2a01da08244c50b20ccfaeb5972e4228c3c3a1989d3ece2bc4b1f996001", size = 11902041 },
211
+ { url = "https://files.pythonhosted.org/packages/53/cf/1fffa09fb518d646f560ccfba59f91b23c731e461d6a4dedd21a393a1ff1/ruff-0.9.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96d5c76358419bc63a671caac70c18732d4fd0341646ecd01641ddda5c39ca0b", size = 11421069 },
212
+ { url = "https://files.pythonhosted.org/packages/09/27/bb8f1b7304e2a9431f631ae7eadc35550fe0cf620a2a6a0fc4aa3d736f94/ruff-0.9.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:deb8304636ed394211f3a6d46c0e7d9535b016f53adaa8340139859b2359a070", size = 12625095 },
213
+ { url = "https://files.pythonhosted.org/packages/d7/ce/ab00bc9d3df35a5f1b64f5117458160a009f93ae5caf65894ebb63a1842d/ruff-0.9.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df455000bf59e62b3e8c7ba5ed88a4a2bc64896f900f311dc23ff2dc38156440", size = 13257797 },
214
+ { url = "https://files.pythonhosted.org/packages/88/81/c639a082ae6d8392bc52256058ec60f493c6a4d06d5505bccface3767e61/ruff-0.9.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de92170dfa50c32a2b8206a647949590e752aca8100a0f6b8cefa02ae29dce80", size = 12763793 },
215
+ { url = "https://files.pythonhosted.org/packages/b3/d0/0a3d8f56d1e49af466dc770eeec5c125977ba9479af92e484b5b0251ce9c/ruff-0.9.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d28532d73b1f3f627ba88e1456f50748b37f3a345d2be76e4c653bec6c3e393", size = 14386234 },
216
+ { url = "https://files.pythonhosted.org/packages/04/70/e59c192a3ad476355e7f45fb3a87326f5219cc7c472e6b040c6c6595c8f0/ruff-0.9.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c746d7d1df64f31d90503ece5cc34d7007c06751a7a3bbeee10e5f2463d52d2", size = 12437505 },
217
+ { url = "https://files.pythonhosted.org/packages/55/4e/3abba60a259d79c391713e7a6ccabf7e2c96e5e0a19100bc4204f1a43a51/ruff-0.9.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11417521d6f2d121fda376f0d2169fb529976c544d653d1d6044f4c5562516ee", size = 11884799 },
218
+ { url = "https://files.pythonhosted.org/packages/a3/db/b0183a01a9f25b4efcae919c18fb41d32f985676c917008620ad692b9d5f/ruff-0.9.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b9d71c3879eb32de700f2f6fac3d46566f644a91d3130119a6378f9312a38e1", size = 11527411 },
219
+ { url = "https://files.pythonhosted.org/packages/0a/e4/3ebfcebca3dff1559a74c6becff76e0b64689cea02b7aab15b8b32ea245d/ruff-0.9.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2e36c61145e70febcb78483903c43444c6b9d40f6d2f800b5552fec6e4a7bb9a", size = 12078868 },
220
+ { url = "https://files.pythonhosted.org/packages/ec/b2/5ab808833e06c0a1b0d046a51c06ec5687b73c78b116e8d77687dc0cd515/ruff-0.9.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2f71d09aeba026c922aa7aa19a08d7bd27c867aedb2f74285a2639644c1c12f5", size = 12524374 },
221
+ { url = "https://files.pythonhosted.org/packages/e0/51/1432afcc3b7aa6586c480142caae5323d59750925c3559688f2a9867343f/ruff-0.9.5-py3-none-win32.whl", hash = "sha256:134f958d52aa6fdec3b294b8ebe2320a950d10c041473c4316d2e7d7c2544723", size = 9853682 },
222
+ { url = "https://files.pythonhosted.org/packages/b7/ad/c7a900591bd152bb47fc4882a27654ea55c7973e6d5d6396298ad3fd6638/ruff-0.9.5-py3-none-win_amd64.whl", hash = "sha256:78cc6067f6d80b6745b67498fb84e87d32c6fc34992b52bffefbdae3442967d6", size = 10865744 },
223
+ { url = "https://files.pythonhosted.org/packages/75/d9/fde7610abd53c0c76b6af72fc679cb377b27c617ba704e25da834e0a0608/ruff-0.9.5-py3-none-win_arm64.whl", hash = "sha256:18a29f1a005bddb229e580795627d297dfa99f16b30c7039e73278cf6b5f9fa9", size = 10064595 },
224
+ ]
225
+
226
+ [[package]]
227
+ name = "tomli"
228
+ version = "2.2.1"
229
+ source = { registry = "https://pypi.org/simple" }
230
+ sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
231
+ wheels = [
232
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
233
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
234
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
235
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
236
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
237
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
238
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
239
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
240
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
241
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
242
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
243
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
244
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
245
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
246
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
247
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
248
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
249
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
250
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
251
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
252
+ { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
253
+ { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
254
+ { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
255
+ { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
256
+ { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
257
+ { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
258
+ { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
259
+ { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
260
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
261
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
262
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
263
+ ]
264
+
265
+ [[package]]
266
+ name = "virtualenv"
267
+ version = "20.29.1"
268
+ source = { registry = "https://pypi.org/simple" }
269
+ dependencies = [
270
+ { name = "distlib" },
271
+ { name = "filelock" },
272
+ { name = "platformdirs" },
273
+ ]
274
+ sdist = { url = "https://files.pythonhosted.org/packages/a7/ca/f23dcb02e161a9bba141b1c08aa50e8da6ea25e6d780528f1d385a3efe25/virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35", size = 7658028 }
275
+ wheels = [
276
+ { url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 },
277
+ ]