mongo-aggro 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.
@@ -0,0 +1,537 @@
1
+ Metadata-Version: 2.4
2
+ Name: mongo-aggro
3
+ Version: 0.1.0
4
+ Summary: MongoDB Aggregation Pipeline Builder with Pydantic
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: mongodb,aggregation,pipeline,pydantic,database
8
+ Author: Hamed Ghenaat
9
+ Requires-Python: >=3.12
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Database
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Typing :: Typed
19
+ Provides-Extra: dev
20
+ Provides-Extra: docs
21
+ Provides-Extra: test
22
+ Requires-Dist: black (>=24.10.0) ; extra == "dev"
23
+ Requires-Dist: isort (>=5.13.2) ; extra == "dev"
24
+ Requires-Dist: mkdocs (>=1.6.0) ; extra == "docs"
25
+ Requires-Dist: mkdocs-material (>=9.5.0) ; extra == "docs"
26
+ Requires-Dist: mkdocstrings (>=0.27.0) ; extra == "docs"
27
+ Requires-Dist: mkdocstrings-python (>=1.12.0) ; extra == "docs"
28
+ Requires-Dist: mypy (>=1.13.0) ; extra == "dev"
29
+ Requires-Dist: pre-commit (>=4.0.0) ; extra == "dev"
30
+ Requires-Dist: pydantic (>=2.10.0)
31
+ Requires-Dist: pytest (>=8.0.0) ; extra == "test"
32
+ Requires-Dist: pytest-cov (>=6.0.0) ; extra == "test"
33
+ Requires-Dist: pytest-mock (>=3.14.0) ; extra == "test"
34
+ Requires-Dist: pyupgrade (>=3.19.0) ; extra == "dev"
35
+ Requires-Dist: ruff (>=0.8.0) ; extra == "dev"
36
+ Project-URL: Documentation, https://hamedghenaat.github.io/mongo-aggro/
37
+ Project-URL: Homepage, https://github.com/hamedghenaat/mongo-aggro
38
+ Project-URL: Repository, https://github.com/hamedghenaat/mongo-aggro
39
+ Description-Content-Type: text/markdown
40
+
41
+ # Mongo Aggro - MongoDB Aggregation Pipeline Builder
42
+
43
+ A Python package for building MongoDB aggregation pipelines with strong type checking using Pydantic.
44
+
45
+ ## Features
46
+
47
+ - **Type-safe pipeline building** - Pydantic models ensure type safety at runtime
48
+ - **Direct MongoDB integration** - Pass `Pipeline` directly to `collection.aggregate()` without calling any methods
49
+ - **Comprehensive stage support** - All major MongoDB aggregation stages supported
50
+ - **Nested pipelines** - Stages like `$lookup`, `$facet`, and `$unionWith` support nested pipelines
51
+ - **Query operators** - Built-in support for logical operators (`$and`, `$or`, `$not`, `$nor`) and comparison operators
52
+ - **Accumulator classes** - Type-safe accumulator builders for `$group` stage
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ poetry add mongo-aggro
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ ```python
63
+ from mongo_aggro import Pipeline, Match, Unwind, Group, Sort, Limit
64
+
65
+ # Create a pipeline
66
+ pipeline = Pipeline()
67
+ pipeline.add_stage(Match(query={"status": "active"}))
68
+ pipeline.add_stage(Unwind(path="items"))
69
+ pipeline.add_stage(Group(id="$category", accumulators={"count": {"$sum": 1}}))
70
+ pipeline.add_stage(Sort(fields={"count": -1}))
71
+ pipeline.add_stage(Limit(count=10))
72
+
73
+ # Pass directly to MongoDB - no need to call any methods
74
+ results = collection.aggregate(pipeline)
75
+ ```
76
+
77
+ Or initialize with stages in the constructor:
78
+
79
+ ```python
80
+ pipeline = Pipeline([
81
+ Match(query={"status": "active"}),
82
+ Unwind(path="items"),
83
+ Group(id="$category", accumulators={"count": {"$sum": 1}}),
84
+ Sort(fields={"count": -1}),
85
+ Limit(count=10)
86
+ ])
87
+ ```
88
+
89
+ ## Supported Stages
90
+
91
+ ### Document Filtering & Transformation
92
+ - **Match** - Filter documents (`$match`)
93
+ - **Project** - Shape documents (`$project`)
94
+ - **AddFields / Set** - Add new fields (`$addFields`, `$set`)
95
+ - **Unset** - Remove fields (`$unset`)
96
+ - **ReplaceRoot / ReplaceWith** - Replace document root (`$replaceRoot`, `$replaceWith`)
97
+ - **Redact** - Restrict document content (`$redact`)
98
+
99
+ ### Grouping & Aggregation
100
+ - **Group** - Group and aggregate (`$group`)
101
+ - **Bucket** - Categorize into buckets (`$bucket`)
102
+ - **BucketAuto** - Auto-categorize into buckets (`$bucketAuto`)
103
+ - **SortByCount** - Group, count, and sort (`$sortByCount`)
104
+ - **Count** - Count documents (`$count`)
105
+
106
+ ### Array Operations
107
+ - **Unwind** - Deconstruct arrays (`$unwind`)
108
+
109
+ ### Sorting & Pagination
110
+ - **Sort** - Sort documents (`$sort`)
111
+ - **Limit** - Limit results (`$limit`)
112
+ - **Skip** - Skip documents (`$skip`)
113
+ - **Sample** - Random sampling (`$sample`)
114
+
115
+ ### Joins & Lookups
116
+ - **Lookup** - Left outer join (`$lookup`)
117
+ - **GraphLookup** - Recursive search (`$graphLookup`)
118
+
119
+ ### Multiple Pipelines
120
+ - **Facet** - Multiple pipelines in single stage (`$facet`)
121
+ - **UnionWith** - Union with another collection (`$unionWith`)
122
+
123
+ ### Output
124
+ - **Out** - Write to collection (`$out`)
125
+ - **Merge** - Merge into collection (`$merge`)
126
+
127
+ ### Geospatial
128
+ - **GeoNear** - Geospatial queries (`$geoNear`)
129
+
130
+ ### Window Functions & Analytics
131
+ - **SetWindowFields** - Window calculations (`$setWindowFields`)
132
+ - **Densify** - Fill gaps in data (`$densify`)
133
+ - **Fill** - Fill null/missing values (`$fill`)
134
+
135
+ ### Utility
136
+ - **Documents** - Return literal documents (`$documents`)
137
+
138
+ ## Accumulators
139
+
140
+ Type-safe accumulator classes for the `$group` stage:
141
+
142
+ ```python
143
+ from mongo_aggro import Sum, Avg, Min, Max, First, Last, Push, AddToSet, Count_
144
+ from mongo_aggro import merge_accumulators
145
+
146
+ # Each accumulator returns a dictionary
147
+ Sum(name="totalQuantity", field="quantity").model_dump()
148
+ # Output: {"totalQuantity": {"$sum": "$quantity"}}
149
+
150
+ Avg(name="avgPrice", field="price").model_dump()
151
+ # Output: {"avgPrice": {"$avg": "$price"}}
152
+
153
+ # Use value=1 for counting
154
+ Sum(name="count", value=1).model_dump()
155
+ # Output: {"count": {"$sum": 1}}
156
+
157
+ # Push with expression
158
+ Push(name="orderDetails", expression={"item": "$item", "qty": "$quantity"}).model_dump()
159
+ # Output: {"orderDetails": {"$push": {"item": "$item", "qty": "$quantity"}}}
160
+ ```
161
+
162
+ ### Merging Accumulators
163
+
164
+ Use `merge_accumulators` to combine multiple accumulators:
165
+
166
+ ```python
167
+ from mongo_aggro import Group, Sum, Avg, Max, Min, merge_accumulators
168
+
169
+ # Merge multiple accumulators for Group stage
170
+ group = Group(
171
+ id="$category",
172
+ accumulators=merge_accumulators(
173
+ Sum(name="totalSales", field="amount"),
174
+ Avg(name="avgPrice", field="price"),
175
+ Max(name="maxPrice", field="price"),
176
+ Min(name="minPrice", field="price"),
177
+ Sum(name="orderCount", value=1)
178
+ )
179
+ )
180
+ # Output: {"$group": {
181
+ # "_id": "$category",
182
+ # "totalSales": {"$sum": "$amount"},
183
+ # "avgPrice": {"$avg": "$price"},
184
+ # "maxPrice": {"$max": "$price"},
185
+ # "minPrice": {"$min": "$price"},
186
+ # "orderCount": {"$sum": 1}
187
+ # }}
188
+ ```
189
+
190
+ ### Available Accumulators
191
+
192
+ | Accumulator | Description |
193
+ |-------------|-------------|
194
+ | `Sum` | Sum values or count with `value=1` |
195
+ | `Avg` | Calculate average |
196
+ | `Min` | Get minimum value |
197
+ | `Max` | Get maximum value |
198
+ | `First` | First value in group |
199
+ | `Last` | Last value in group |
200
+ | `Push` | Create array of values |
201
+ | `AddToSet` | Create array of unique values |
202
+ | `StdDevPop` | Population standard deviation |
203
+ | `StdDevSamp` | Sample standard deviation |
204
+ | `Count_` | Count documents (MongoDB 5.0+) |
205
+ | `MergeObjects` | Merge documents |
206
+ | `TopN` | Top N elements (MongoDB 5.2+) |
207
+ | `BottomN` | Bottom N elements (MongoDB 5.2+) |
208
+ | `FirstN` | First N elements (MongoDB 5.2+) |
209
+ | `LastN` | Last N elements (MongoDB 5.2+) |
210
+ | `MaxN` | N maximum values (MongoDB 5.2+) |
211
+ | `MinN` | N minimum values (MongoDB 5.2+) |
212
+
213
+ ## Examples
214
+
215
+ ### Complex Match with Logical Operators
216
+
217
+ ```python
218
+ from mongo_aggro import Match, And, Or
219
+
220
+ # Using $and and $or directly in query dict
221
+ match = Match(query={
222
+ "$and": [
223
+ {"status": "active"},
224
+ {"$or": [
225
+ {"type": "premium"},
226
+ {"balance": {"$gt": 1000}}
227
+ ]}
228
+ ]
229
+ })
230
+
231
+ # Or use operator classes for building conditions
232
+ and_cond = And(conditions=[
233
+ {"status": "active"},
234
+ {"age": {"$gte": 18}}
235
+ ])
236
+ print(and_cond.model_dump()) # {"$and": [{"status": "active"}, {"age": {"$gte": 18}}]}
237
+
238
+ # Complex nested conditions
239
+ or_cond = Or(conditions=[
240
+ {"region": "US"},
241
+ And(conditions=[{"region": "EU"}, {"premium": True}]).model_dump()
242
+ ])
243
+ ```
244
+
245
+ ### Combining Stages with Operators
246
+
247
+ ```python
248
+ from mongo_aggro import (
249
+ Pipeline, Match, Group, Sort, Limit, Project,
250
+ And, Or, Expr, In, Gt, Regex,
251
+ Sum, Avg, Max, merge_accumulators
252
+ )
253
+
254
+ # Build a complex analytics pipeline
255
+ pipeline = Pipeline()
256
+
257
+ # Stage 1: Match with complex conditions
258
+ pipeline.add_stage(Match(query={
259
+ "$and": [
260
+ {"status": {"$in": ["completed", "shipped"]}},
261
+ {"orderDate": {"$gte": "2024-01-01"}},
262
+ {"$or": [
263
+ {"totalAmount": {"$gt": 100}},
264
+ {"priority": "high"}
265
+ ]}
266
+ ]
267
+ }))
268
+
269
+ # Stage 2: Group with multiple accumulators
270
+ pipeline.add_stage(Group(
271
+ id={"region": "$region", "category": "$category"},
272
+ accumulators=merge_accumulators(
273
+ Sum(name="totalRevenue", field="totalAmount"),
274
+ Avg(name="avgOrderValue", field="totalAmount"),
275
+ Max(name="largestOrder", field="totalAmount"),
276
+ Sum(name="orderCount", value=1)
277
+ )
278
+ ))
279
+
280
+ # Stage 3: Match groups with significant revenue
281
+ pipeline.add_stage(Match(query={
282
+ "totalRevenue": {"$gt": 10000}
283
+ }))
284
+
285
+ # Stage 4: Sort by revenue
286
+ pipeline.add_stage(Sort(fields={"totalRevenue": -1}))
287
+
288
+ # Stage 5: Limit results
289
+ pipeline.add_stage(Limit(count=20))
290
+
291
+ # Stage 6: Project final shape
292
+ pipeline.add_stage(Project(fields={
293
+ "_id": 0,
294
+ "region": "$_id.region",
295
+ "category": "$_id.category",
296
+ "totalRevenue": 1,
297
+ "avgOrderValue": {"$round": ["$avgOrderValue", 2]},
298
+ "orderCount": 1
299
+ }))
300
+ ```
301
+
302
+ ### Lookup with Nested Pipeline
303
+
304
+ ```python
305
+ from mongo_aggro import Pipeline, Match, Lookup
306
+
307
+ lookup = Lookup(
308
+ from_collection="orders",
309
+ let={"customerId": "$_id"},
310
+ pipeline=Pipeline([
311
+ Match(query={"$expr": {"$eq": ["$customerId", "$$customerId"]}}),
312
+ Match(query={"status": "completed"})
313
+ ]),
314
+ as_field="completedOrders"
315
+ )
316
+ ```
317
+
318
+ ### Facet with Multiple Pipelines
319
+
320
+ ```python
321
+ from mongo_aggro import Pipeline, Facet, Group, Sort, Limit, Sum, merge_accumulators
322
+
323
+ facet = Facet(pipelines={
324
+ "byCategory": Pipeline([
325
+ Group(
326
+ id="$category",
327
+ accumulators=merge_accumulators(
328
+ Sum(name="count", value=1),
329
+ Sum(name="total", field="amount")
330
+ )
331
+ ),
332
+ Sort(fields={"count": -1})
333
+ ]),
334
+ "byRegion": Pipeline([
335
+ Group(
336
+ id="$region",
337
+ accumulators=merge_accumulators(
338
+ Sum(name="count", value=1),
339
+ Avg(name="avgAmount", field="amount")
340
+ )
341
+ ),
342
+ Sort(fields={"avgAmount": -1})
343
+ ]),
344
+ "topProducts": Pipeline([
345
+ Sort(fields={"sales": -1}),
346
+ Limit(count=10)
347
+ ])
348
+ })
349
+ ```
350
+
351
+ ### Using Query Operators with Match
352
+
353
+ ```python
354
+ from mongo_aggro import Match, Regex, In, Exists, ElemMatch
355
+
356
+ # Text search with regex
357
+ pipeline.add_stage(Match(query={
358
+ "name": Regex(pattern="^John", options="i").model_dump()
359
+ }))
360
+
361
+ # Check field existence
362
+ pipeline.add_stage(Match(query={
363
+ "email": Exists(exists=True).model_dump(),
364
+ "deletedAt": Exists(exists=False).model_dump()
365
+ }))
366
+
367
+ # Array element matching
368
+ pipeline.add_stage(Match(query={
369
+ "items": ElemMatch(conditions={
370
+ "quantity": {"$gt": 5},
371
+ "price": {"$lt": 100}
372
+ }).model_dump()
373
+ }))
374
+
375
+ # Combining multiple operator types
376
+ pipeline.add_stage(Match(query={
377
+ "$and": [
378
+ {"status": In(values=["active", "pending"]).model_dump()},
379
+ {"score": Gt(value=80).model_dump()},
380
+ {"tags": {"$exists": True}}
381
+ ]
382
+ }))
383
+ ```
384
+
385
+ ### Unwind with Options
386
+
387
+ ```python
388
+ from mongo_aggro import Unwind
389
+
390
+ # Simple unwind
391
+ unwind = Unwind(path="items")
392
+ # Output: {"$unwind": "$items"}
393
+
394
+ # With options
395
+ unwind = Unwind(
396
+ path="items",
397
+ include_array_index="itemIndex",
398
+ preserve_null_and_empty=True
399
+ )
400
+ # Output: {"$unwind": {"path": "$items", "includeArrayIndex": "itemIndex", "preserveNullAndEmptyArrays": true}}
401
+ ```
402
+
403
+ ### Group with Accumulators
404
+
405
+ ```python
406
+ from mongo_aggro import Group, Sum, Avg, Max, Push, merge_accumulators
407
+
408
+ group = Group(
409
+ id="$category",
410
+ accumulators=merge_accumulators(
411
+ Sum(name="totalQuantity", field="quantity"),
412
+ Avg(name="avgPrice", field="price"),
413
+ Max(name="maxPrice", field="price"),
414
+ Push(name="items", field="name")
415
+ )
416
+ )
417
+ ```
418
+
419
+ ### Complete E-commerce Analytics Example
420
+
421
+ ```python
422
+ from mongo_aggro import (
423
+ Pipeline, Match, Unwind, Group, Sort, Limit, Project, Lookup,
424
+ Sum, Avg, Max, First, Push, merge_accumulators
425
+ )
426
+
427
+ # Analyze orders with customer details
428
+ pipeline = Pipeline([
429
+ # Filter recent orders
430
+ Match(query={
431
+ "orderDate": {"$gte": "2024-01-01"},
432
+ "status": {"$ne": "cancelled"}
433
+ }),
434
+
435
+ # Join with customers
436
+ Lookup(
437
+ from_collection="customers",
438
+ local_field="customerId",
439
+ foreign_field="_id",
440
+ as_field="customer"
441
+ ),
442
+
443
+ # Unwind customer (single element array)
444
+ Unwind(path="customer"),
445
+
446
+ # Unwind order items
447
+ Unwind(path="items"),
448
+
449
+ # Group by product and customer region
450
+ Group(
451
+ id={
452
+ "product": "$items.productId",
453
+ "region": "$customer.region"
454
+ },
455
+ accumulators=merge_accumulators(
456
+ Sum(name="totalQuantity", field="items.quantity"),
457
+ Sum(name="totalRevenue", field="items.subtotal"),
458
+ Avg(name="avgQuantity", field="items.quantity"),
459
+ Sum(name="orderCount", value=1),
460
+ First(name="productName", field="items.name")
461
+ )
462
+ ),
463
+
464
+ # Filter significant sales
465
+ Match(query={"totalRevenue": {"$gt": 1000}}),
466
+
467
+ # Sort by revenue
468
+ Sort(fields={"totalRevenue": -1}),
469
+
470
+ # Top 50 results
471
+ Limit(count=50),
472
+
473
+ # Final projection
474
+ Project(fields={
475
+ "_id": 0,
476
+ "product": "$productName",
477
+ "region": "$_id.region",
478
+ "totalQuantity": 1,
479
+ "totalRevenue": {"$round": ["$totalRevenue", 2]},
480
+ "avgQuantity": {"$round": ["$avgQuantity", 1]},
481
+ "orderCount": 1
482
+ })
483
+ ])
484
+
485
+ # Execute
486
+ results = db.orders.aggregate(pipeline)
487
+ ```
488
+
489
+ ## Query Operators
490
+
491
+ The package includes query operators for building complex conditions:
492
+
493
+ ```python
494
+ from mongo_aggro import And, Or, Not, Nor, Expr, Eq, Gt, In, Regex
495
+
496
+ # Logical operators
497
+ And(conditions=[{"a": 1}, {"b": 2}]).model_dump() # {"$and": [...]}
498
+ Or(conditions=[{"a": 1}, {"a": 2}]).model_dump() # {"$or": [...]}
499
+ Not(condition={"$regex": "^test"}).model_dump() # {"$not": {...}}
500
+ Nor(conditions=[{"a": 1}, {"b": 2}]).model_dump() # {"$nor": [...]}
501
+
502
+ # Comparison operators
503
+ Eq(value=5).model_dump() # {"$eq": 5}
504
+ Gt(value=10).model_dump() # {"$gt": 10}
505
+ In(values=[1, 2, 3]).model_dump() # {"$in": [1, 2, 3]}
506
+ Regex(pattern="^test", options="i").model_dump() # {"$regex": "^test", "$options": "i"}
507
+ ```
508
+
509
+ ## Method Chaining
510
+
511
+ The `add_stage` method returns the pipeline, enabling method chaining:
512
+
513
+ ```python
514
+ pipeline = (
515
+ Pipeline()
516
+ .add_stage(Match(query={"active": True}))
517
+ .add_stage(Sort(fields={"createdAt": -1}))
518
+ .add_stage(Limit(count=100))
519
+ )
520
+ ```
521
+
522
+ ## How It Works
523
+
524
+ The `Pipeline` class implements `__iter__`, which yields each stage's dictionary representation when iterated. MongoDB's `aggregate()` method iterates over the pipeline argument, so no conversion is needed:
525
+
526
+ ```python
527
+ # This works because MongoDB iterates over the pipeline
528
+ collection.aggregate(pipeline)
529
+
530
+ # Equivalent to:
531
+ collection.aggregate([
532
+ {"$match": {"status": "active"}},
533
+ {"$unwind": "$items"},
534
+ # ...
535
+ ])
536
+ ```
537
+
@@ -0,0 +1,9 @@
1
+ mongo_aggro/__init__.py,sha256=pTbpK7bdK5iDITTkalZSma4dax_-g_bqJjseXhx5xU0,2960
2
+ mongo_aggro/accumulators.py,sha256=v1UR0R8Sgx5K3aZRntP-G4jhRlQ2Ff189cadMOt2Gwk,14241
3
+ mongo_aggro/base.py,sha256=76BwUUfyWNDLnRnRX6xt2NcRgdj7u1enk4QZ4ywl9Fg,6090
4
+ mongo_aggro/operators.py,sha256=-PnF-R65usJfvatktLGUoXaPbdq6uhxHJSIX66nb7GQ,6657
5
+ mongo_aggro/stages.py,sha256=sMglloJn5wAnDs3CjWleFU_cse3jGhE348VsTcvBI44,30320
6
+ mongo_aggro-0.1.0.dist-info/METADATA,sha256=qQq3mvw_6NNQyDyecVhBBbQKpmmAEKAEbE1oYnJkTzY,15377
7
+ mongo_aggro-0.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
8
+ mongo_aggro-0.1.0.dist-info/licenses/LICENSE,sha256=XnrHxv3Dgf-ttUmsrNPMnQ69JLMED67bxKk3zrOdB40,1070
9
+ mongo_aggro-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hamed Ghenaat
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.