AxiomQuery 0.1.0__py3-none-any.whl
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.
- axiom_query/__init__.py +22 -0
- axiom_query/aggregation.py +111 -0
- axiom_query/aggregation_parser.py +186 -0
- axiom_query/ast.py +64 -0
- axiom_query/compiler.py +207 -0
- axiom_query/compiler_aggregate.py +358 -0
- axiom_query/engine.py +210 -0
- axiom_query/errors.py +12 -0
- axiom_query/operators.py +30 -0
- axiom_query/parser.py +102 -0
- axiom_query/py.typed +0 -0
- axiom_query/schema.py +55 -0
- axiomquery-0.1.0.dist-info/METADATA +232 -0
- axiomquery-0.1.0.dist-info/RECORD +16 -0
- axiomquery-0.1.0.dist-info/WHEEL +4 -0
- axiomquery-0.1.0.dist-info/licenses/LICENSE +21 -0
axiom_query/parser.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Parse frontend JSON domain expressions into QuerySpec AST nodes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from axiom_query.ast import And, Bool, Condition, Not, Or, QuerySpec
|
|
8
|
+
from axiom_query.operators import Op
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_domain(raw: Any) -> QuerySpec:
|
|
12
|
+
"""Parse a frontend domain expression into a QuerySpec AST."""
|
|
13
|
+
from axiom_query.errors import QueryError
|
|
14
|
+
|
|
15
|
+
if raw is None:
|
|
16
|
+
return Bool(True)
|
|
17
|
+
if isinstance(raw, list):
|
|
18
|
+
return _parse_list(raw)
|
|
19
|
+
if isinstance(raw, dict):
|
|
20
|
+
return _parse_dict(raw)
|
|
21
|
+
raise QueryError(
|
|
22
|
+
"INVALID_DOMAIN",
|
|
23
|
+
f"Domain must be a list or dict, got {type(raw).__name__}",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parse_list(items: list) -> QuerySpec:
|
|
28
|
+
if not items:
|
|
29
|
+
return Bool(True)
|
|
30
|
+
|
|
31
|
+
specs = [_parse_item(item) for item in items]
|
|
32
|
+
result = specs[0]
|
|
33
|
+
for s in specs[1:]:
|
|
34
|
+
result = And(left=result, right=s)
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_item(item: Any) -> QuerySpec:
|
|
39
|
+
from axiom_query.errors import QueryError
|
|
40
|
+
|
|
41
|
+
if isinstance(item, (list, tuple)) and len(item) == 3:
|
|
42
|
+
field_path, op_str, value = item
|
|
43
|
+
if not isinstance(field_path, str):
|
|
44
|
+
raise QueryError(
|
|
45
|
+
"INVALID_DOMAIN",
|
|
46
|
+
f"Field path must be a string, got {type(field_path).__name__}",
|
|
47
|
+
)
|
|
48
|
+
try:
|
|
49
|
+
op = Op.from_str(str(op_str))
|
|
50
|
+
except ValueError:
|
|
51
|
+
raise QueryError("INVALID_DOMAIN", f"Unknown operator: {op_str!r}")
|
|
52
|
+
return Condition(field_path=field_path, operator=op, value=value)
|
|
53
|
+
|
|
54
|
+
if isinstance(item, dict):
|
|
55
|
+
return _parse_dict(item)
|
|
56
|
+
|
|
57
|
+
raise QueryError(
|
|
58
|
+
"INVALID_DOMAIN",
|
|
59
|
+
f"Each condition must be [field, op, value] or a logical dict, got {type(item).__name__}",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_dict(d: dict) -> QuerySpec:
|
|
64
|
+
from axiom_query.errors import QueryError
|
|
65
|
+
|
|
66
|
+
if len(d) != 1:
|
|
67
|
+
raise QueryError(
|
|
68
|
+
"INVALID_DOMAIN",
|
|
69
|
+
"Logical dict must have exactly one key: 'and', 'or', or 'not'",
|
|
70
|
+
)
|
|
71
|
+
key = next(iter(d))
|
|
72
|
+
val = d[key]
|
|
73
|
+
|
|
74
|
+
if key == "and":
|
|
75
|
+
if not isinstance(val, list) or len(val) < 2:
|
|
76
|
+
raise QueryError(
|
|
77
|
+
"INVALID_DOMAIN", "'and' requires a list of at least 2 items"
|
|
78
|
+
)
|
|
79
|
+
specs = [_parse_item(item) for item in val]
|
|
80
|
+
result = specs[0]
|
|
81
|
+
for s in specs[1:]:
|
|
82
|
+
result = And(left=result, right=s)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
if key == "or":
|
|
86
|
+
if not isinstance(val, list) or len(val) < 2:
|
|
87
|
+
raise QueryError(
|
|
88
|
+
"INVALID_DOMAIN", "'or' requires a list of at least 2 items"
|
|
89
|
+
)
|
|
90
|
+
specs = [_parse_item(item) for item in val]
|
|
91
|
+
result = specs[0]
|
|
92
|
+
for s in specs[1:]:
|
|
93
|
+
result = Or(left=result, right=s)
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
if key == "not":
|
|
97
|
+
return Not(operand=_parse_item(val))
|
|
98
|
+
|
|
99
|
+
raise QueryError(
|
|
100
|
+
"INVALID_DOMAIN",
|
|
101
|
+
f"Unknown logical key: {key!r}. Use 'and', 'or', or 'not'.",
|
|
102
|
+
)
|
axiom_query/py.typed
ADDED
|
File without changes
|
axiom_query/schema.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""ModelSchema — derives table/column/child metadata from SA ORM models via inspect()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import Column, Table
|
|
8
|
+
from sqlalchemy import inspect as sa_inspect
|
|
9
|
+
from sqlalchemy.orm import RelationshipDirection
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ChildSchema:
|
|
14
|
+
name: str
|
|
15
|
+
table: Table
|
|
16
|
+
fk_field: str
|
|
17
|
+
columns: dict[str, Column]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ModelSchema:
|
|
22
|
+
model_class: type
|
|
23
|
+
table: Table
|
|
24
|
+
columns: dict[str, Column]
|
|
25
|
+
children: dict[str, ChildSchema] = field(default_factory=dict)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def derive_schema(model_class: type) -> ModelSchema:
|
|
29
|
+
"""Derive a ModelSchema from a SA ORM model class using inspect()."""
|
|
30
|
+
mapper = sa_inspect(model_class)
|
|
31
|
+
table = mapper.local_table
|
|
32
|
+
columns = {col.key: col for col in mapper.columns}
|
|
33
|
+
|
|
34
|
+
children: dict[str, ChildSchema] = {}
|
|
35
|
+
for rel_name, rel in mapper.relationships.items():
|
|
36
|
+
if rel.direction == RelationshipDirection.ONETOMANY:
|
|
37
|
+
child_table = rel.mapper.local_table
|
|
38
|
+
child_columns = {col.key: col for col in rel.mapper.columns}
|
|
39
|
+
# Find the FK column on the child table via synchronize_pairs
|
|
40
|
+
# synchronize_pairs: list of (parent_col, child_col) tuples
|
|
41
|
+
_, child_fk_col = next(iter(rel.synchronize_pairs))
|
|
42
|
+
fk_field = child_fk_col.key
|
|
43
|
+
children[rel_name] = ChildSchema(
|
|
44
|
+
name=rel_name,
|
|
45
|
+
table=child_table,
|
|
46
|
+
fk_field=fk_field,
|
|
47
|
+
columns=child_columns,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return ModelSchema(
|
|
51
|
+
model_class=model_class,
|
|
52
|
+
table=table,
|
|
53
|
+
columns=columns,
|
|
54
|
+
children=children,
|
|
55
|
+
)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: AxiomQuery
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Specification-based query and aggregation engine for SQLAlchemy 2.0 ORM models
|
|
5
|
+
Project-URL: Source Code, https://github.com/Axiom-Dev-Labs/AxiomQuery
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/Axiom-Dev-Labs/AxiomQuery/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/Axiom-Dev-Labs/AxiomQuery/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Axiom Contributors
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Axiom Contributors
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: domain,filter,groupby,orm,query,specification,sqlalchemy
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
37
|
+
Classifier: Topic :: Database
|
|
38
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
39
|
+
Classifier: Typing :: Typed
|
|
40
|
+
Requires-Python: >=3.12
|
|
41
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# AxiomQuery
|
|
45
|
+
|
|
46
|
+
Specification-based query and aggregation engine for SQLAlchemy 2.0 ORM models.
|
|
47
|
+
|
|
48
|
+
Define filters as composable data — JSON lists, dicts, or Python AST nodes — and execute them against any ORM model without writing raw SQL.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install AxiomQuery
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Requires Python 3.12+ and SQLAlchemy 2.0+.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from axiom_query import QueryEngine
|
|
66
|
+
|
|
67
|
+
engine = QueryEngine(Order) # inspect() once — no DB connection at construction
|
|
68
|
+
|
|
69
|
+
with Session(db) as session:
|
|
70
|
+
# list
|
|
71
|
+
records = engine.list(session, domain=[["status", "=", "CONFIRMED"]])
|
|
72
|
+
|
|
73
|
+
# read_group
|
|
74
|
+
groups, total = engine.read_group(
|
|
75
|
+
session,
|
|
76
|
+
groupby=["status"],
|
|
77
|
+
aggregates=["__count", "total:sum"],
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Domain filter syntax
|
|
84
|
+
|
|
85
|
+
A **domain** is a JSON-serialisable expression compiled to a WHERE clause at query time.
|
|
86
|
+
|
|
87
|
+
### Condition tuple
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
[field_path, operator, value]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| Operator | Meaning |
|
|
94
|
+
|----------|---------|
|
|
95
|
+
| `=` `!=` `>` `<` `>=` `<=` | Comparison |
|
|
96
|
+
| `in` `not in` | Membership (value is a list) |
|
|
97
|
+
| `like` `ilike` | Pattern match (`%` wildcard) |
|
|
98
|
+
| `is_null` | Null check (value is `True`/`False`) |
|
|
99
|
+
|
|
100
|
+
### Logical composition
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
# AND — list of conditions (implicit)
|
|
104
|
+
[["status", "=", "CONFIRMED"], ["total", ">", 100]]
|
|
105
|
+
|
|
106
|
+
# AND — explicit
|
|
107
|
+
{"and": [["status", "=", "CONFIRMED"], ["total", ">", 100]]}
|
|
108
|
+
|
|
109
|
+
# OR
|
|
110
|
+
{"or": [["status", "=", "DRAFT"], ["status", "=", "CANCELLED"]]}
|
|
111
|
+
|
|
112
|
+
# NOT
|
|
113
|
+
{"not": ["status", "=", "CANCELLED"]}
|
|
114
|
+
|
|
115
|
+
# Combined — list mixes plain conditions with logical dicts
|
|
116
|
+
[
|
|
117
|
+
{"or": [["status", "=", "CONFIRMED"], ["status", "=", "DRAFT"]]},
|
|
118
|
+
{"not": ["total", "=", 0]},
|
|
119
|
+
]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Child field (EXISTS subquery)
|
|
123
|
+
|
|
124
|
+
Filter parent records by a child relationship field using dot notation. O2M relationships are automatically detected via `inspect()`.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# Orders that have at least one line with quantity > 2
|
|
128
|
+
engine.list(session, domain=[["lines.quantity", ">", 2]])
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## `list()` — filtered records
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
records = engine.list(
|
|
137
|
+
session,
|
|
138
|
+
domain=None, # domain expression or None (all records)
|
|
139
|
+
limit=None, # max records to return
|
|
140
|
+
offset=None, # records to skip
|
|
141
|
+
order_by=None, # [["field", "asc|desc"], ...]
|
|
142
|
+
)
|
|
143
|
+
# returns list[ORM model instances]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## `read_group()` — grouped aggregation
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
groups, total = engine.read_group(
|
|
152
|
+
session,
|
|
153
|
+
groupby=["status", "created_at:month"], # field or field:granularity
|
|
154
|
+
aggregates=["__count", "total:sum"], # __count or field:func
|
|
155
|
+
domain=None, # WHERE filter
|
|
156
|
+
having=None, # HAVING filter on aggregate aliases
|
|
157
|
+
order_by=None, # [["alias", "asc|desc"], ...]
|
|
158
|
+
limit=None,
|
|
159
|
+
offset=None,
|
|
160
|
+
)
|
|
161
|
+
# returns (list[dict], int) — each dict includes a __domain key
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Aggregate functions:** `count` `sum` `avg` `min` `max`
|
|
165
|
+
|
|
166
|
+
**Date granularities:** `day` `week` `month` `quarter` `year`
|
|
167
|
+
|
|
168
|
+
**Child aggregate** (LEFT JOIN):
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
engine.read_group(session, groupby=["status"], aggregates=["lines.quantity:sum"])
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**`__domain` drill-down** — each group result includes a `__domain` ready to pass back to `list()`:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
groups, _ = engine.read_group(session, groupby=["status"], aggregates=["__count"])
|
|
178
|
+
for group in groups:
|
|
179
|
+
records = engine.list(session, domain=group["__domain"])
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Async API
|
|
185
|
+
|
|
186
|
+
Prefix any method with `a` and pass an `AsyncSession`:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
engine = QueryEngine(Order)
|
|
190
|
+
|
|
191
|
+
async with AsyncSession(db) as session:
|
|
192
|
+
records = await engine.alist(session, domain=[["status", "=", "CONFIRMED"]])
|
|
193
|
+
groups, total = await engine.aread_group(session, groupby=["status"], aggregates=["__count"])
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Schema derivation
|
|
199
|
+
|
|
200
|
+
`QueryEngine` derives its schema from `inspect(model_class)` at construction time — no separate descriptor needed:
|
|
201
|
+
|
|
202
|
+
- **Columns** → from `mapper.columns`
|
|
203
|
+
- **Child relations** → O2M relationships (`RelationshipDirection.ONETOMANY`) become filterable child entities
|
|
204
|
+
- **FK column** → resolved from `rel.synchronize_pairs`
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Error handling
|
|
209
|
+
|
|
210
|
+
Invalid field paths and unsupported operators raise `QueryError` before hitting the database:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from axiom_query import QueryError
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
engine.list(session, domain=[["unknown_field", "=", "x"]])
|
|
217
|
+
except QueryError as e:
|
|
218
|
+
print(e.code, e.message) # INVALID_FILTER_FIELD No field 'unknown_field' ...
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Examples
|
|
224
|
+
|
|
225
|
+
Self-contained runnable examples in [`examples/`](examples/):
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
python examples/example_sync.py
|
|
229
|
+
python examples/example_async.py
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Both cover: simple filters, AND / OR / NOT, combined nesting, child EXISTS filtering, pagination, `read_group` with domain / date granularity / child aggregation / HAVING, and `__domain` drill-down.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
axiom_query/__init__.py,sha256=Pe09GG6TA3lCwVxzkUfiracTrdRzwQ3FlDnS2fiEa9E,512
|
|
2
|
+
axiom_query/aggregation.py,sha256=vCYhKXjxg8aHe43SJamZewV-GbyCYDWNElR8-G9mskc,2816
|
|
3
|
+
axiom_query/aggregation_parser.py,sha256=6mtt72Cr8UlmemO6PDELdRpBJMtNI0N0XNpNXfXQx4w,5930
|
|
4
|
+
axiom_query/ast.py,sha256=KWaI5QD9WlW-A7GinIH-KkSaz2vEST4Vx2Kf80GJYj4,1211
|
|
5
|
+
axiom_query/compiler.py,sha256=jfhctH64i0fIVBdtde97-dLOgp39Miuh_mp57uX_g6Y,7326
|
|
6
|
+
axiom_query/compiler_aggregate.py,sha256=ksmnO8PypGDc08OdNM7N_1s4aL5E_ePH6eniqKqvJFw,11965
|
|
7
|
+
axiom_query/engine.py,sha256=NWPVqgbolCpXgjBhJVZzpIfFPqD_u86WkQVPHVZ_j54,6765
|
|
8
|
+
axiom_query/errors.py,sha256=OeID6aSz11fr4cPdy6w46vj0Kf8wDC4g7yTd0pT9_dY,312
|
|
9
|
+
axiom_query/operators.py,sha256=iUryrmDXscvt9tmKaD9A9y6kAONu8RxLbi577csmlyQ,684
|
|
10
|
+
axiom_query/parser.py,sha256=IcWPmePL2O9zKTpMJp07XFueVFmFQ60bHz3oAXk7TfQ,3008
|
|
11
|
+
axiom_query/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
axiom_query/schema.py,sha256=BFsWa2smz0mLeKDjqv-tapTW0nLO8gDJw1gHEOq3am8,1703
|
|
13
|
+
axiomquery-0.1.0.dist-info/METADATA,sha256=VAahceYfepjBmZPu8eWZmg8GrBl8Tdcd5q0_OG4-MSE,7057
|
|
14
|
+
axiomquery-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
15
|
+
axiomquery-0.1.0.dist-info/licenses/LICENSE,sha256=zZPLjk0vKhf5_He43a8zvG1zbw0fbB_A7R2tdWaMLyo,1075
|
|
16
|
+
axiomquery-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Axiom Contributors
|
|
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.
|