python-bestbuy 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. python_bestbuy-0.1.0/.gitignore +77 -0
  2. python_bestbuy-0.1.0/LICENSE +21 -0
  3. python_bestbuy-0.1.0/PKG-INFO +544 -0
  4. python_bestbuy-0.1.0/README.md +516 -0
  5. python_bestbuy-0.1.0/bestbuy/__init__.py +0 -0
  6. python_bestbuy-0.1.0/bestbuy/clients/__init__.py +9 -0
  7. python_bestbuy-0.1.0/bestbuy/clients/base.py +178 -0
  8. python_bestbuy-0.1.0/bestbuy/clients/catalog.py +64 -0
  9. python_bestbuy-0.1.0/bestbuy/clients/commerce.py +74 -0
  10. python_bestbuy-0.1.0/bestbuy/configs/__init__.py +5 -0
  11. python_bestbuy-0.1.0/bestbuy/configs/base.py +14 -0
  12. python_bestbuy-0.1.0/bestbuy/configs/catalog.py +6 -0
  13. python_bestbuy-0.1.0/bestbuy/configs/commerce.py +10 -0
  14. python_bestbuy-0.1.0/bestbuy/exceptions.py +59 -0
  15. python_bestbuy-0.1.0/bestbuy/loggers.py +11 -0
  16. python_bestbuy-0.1.0/bestbuy/models/__init__.py +205 -0
  17. python_bestbuy-0.1.0/bestbuy/models/catalog.py +948 -0
  18. python_bestbuy-0.1.0/bestbuy/models/commerce.py +603 -0
  19. python_bestbuy-0.1.0/bestbuy/operations/__init__.py +50 -0
  20. python_bestbuy-0.1.0/bestbuy/operations/base.py +10 -0
  21. python_bestbuy-0.1.0/bestbuy/operations/catalog.py +1443 -0
  22. python_bestbuy-0.1.0/bestbuy/operations/commerce.py +1208 -0
  23. python_bestbuy-0.1.0/bestbuy/operations/pagination.py +237 -0
  24. python_bestbuy-0.1.0/bestbuy/py.typed +0 -0
  25. python_bestbuy-0.1.0/bestbuy/typing.py +4 -0
  26. python_bestbuy-0.1.0/bestbuy/utils/__init__.py +9 -0
  27. python_bestbuy-0.1.0/bestbuy/utils/encryption.py +103 -0
  28. python_bestbuy-0.1.0/bestbuy/utils/errors.py +164 -0
  29. python_bestbuy-0.1.0/pyproject.toml +73 -0
@@ -0,0 +1,77 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+
5
+ # C extensions
6
+ *.so
7
+
8
+ # Distribution / packaging
9
+ .Python
10
+ venv/
11
+ .env
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+ # PyInstaller
28
+ # Usually these files are written by a python script from a template
29
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
30
+ *.manifest
31
+ *.spec
32
+
33
+ # Installer logs
34
+ pip-log.txt
35
+ pip-delete-this-directory.txt
36
+
37
+ # Unit test / coverage reports
38
+ htmlcov/
39
+ .tox/
40
+ .coverage
41
+ .coverage.*
42
+ .cache
43
+ nosetests.xml
44
+ coverage.xml
45
+ *,cover
46
+ cover/
47
+
48
+ # Translations
49
+ #*.mo
50
+ *.pot
51
+
52
+ # Django stuff:
53
+ *.db
54
+ *.log
55
+
56
+ # Sphinx documentation
57
+ docs/_build/
58
+
59
+ # PyBuilder
60
+ target/
61
+
62
+ # PyCharm
63
+ .idea/
64
+
65
+ # Node
66
+ node_modules/
67
+ _build
68
+
69
+ /media
70
+ /static
71
+ .vscode/
72
+
73
+
74
+ .DS_Store
75
+ tags
76
+ CLAUDE.md
77
+ .python-version
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Michael Lavers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,544 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-bestbuy
3
+ Version: 0.1.0
4
+ Summary: Python client library for Best Buy's Catalog and Commerce APIs
5
+ Project-URL: Homepage, https://github.com/bbify/python-bestbuy
6
+ Project-URL: Repository, https://github.com/bbify/python-bestbuy
7
+ Project-URL: Issues, https://github.com/bbify/python-bestbuy/issues
8
+ Project-URL: Changelog, https://github.com/bbify/python-bestbuy/blob/main/CHANGELOG.md
9
+ Author-email: Michael Lavers <kolanos@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,bestbuy,catalog,commerce,ecommerce
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: cryptography>=35.0.0
25
+ Requires-Dist: httpx>=0.23.0
26
+ Requires-Dist: pydantic-xml>=2.0.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # python-bestbuy
30
+
31
+ [![CI](https://github.com/bbify/python-bestbuy/actions/workflows/ci.yml/badge.svg)](https://github.com/bbify/python-bestbuy/actions/workflows/ci.yml)
32
+
33
+ A Python client library for Best Buy's APIs, providing synchronous and asynchronous HTTP clients for interacting with Best Buy's Catalog and Commerce services.
34
+
35
+ ## Features
36
+
37
+ - **Dual API Support**: Full support for both the Catalog API (product information, categories, stores, recommendations) and Commerce API (orders, fulfillment, pricing)
38
+ - **Sync and Async Clients**: Both synchronous and asynchronous clients for each API
39
+ - **Automatic Pagination**: Built-in paginators for iterating over large result sets
40
+ - **Type Safety**: Full Pydantic model validation for requests and responses
41
+ - **Session Management**: Automatic session handling for Commerce API authentication
42
+ - **Payment Encryption**: Built-in utilities for encrypting credit card data for guest orders
43
+ - **Sandbox Support**: Easy switching between production and sandbox environments
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install python-bestbuy
49
+ ```
50
+
51
+ Or using uv:
52
+
53
+ ```bash
54
+ uv add python-bestbuy
55
+ ```
56
+
57
+ ### Requirements
58
+
59
+ - Python 3.10+
60
+ - httpx
61
+ - pydantic-xml
62
+ - cryptography
63
+
64
+ ## Usage
65
+
66
+ ### Catalog API
67
+
68
+ The Catalog API provides access to Best Buy's product catalog, categories, store locations, and product recommendations.
69
+
70
+ ```python
71
+ from bestbuy.clients.catalog import CatalogClient, AsyncCatalogClient
72
+
73
+ # Initialize the client
74
+ client = CatalogClient(api_key="your-api-key")
75
+ ```
76
+
77
+ #### Products
78
+
79
+ ```python
80
+ # Get a single product by SKU
81
+ product = client.products.get(sku=6487435)
82
+ print(f"{product.name}: ${product.sale_price}")
83
+
84
+ # Get specific attributes only
85
+ product = client.products.get(sku=6487435, show=["name", "salePrice", "manufacturer"])
86
+
87
+ # Search for products
88
+ results = client.products.search(
89
+ query="manufacturer=apple&salePrice<1000",
90
+ show=["sku", "name", "salePrice"],
91
+ sort="salePrice",
92
+ sort_order="asc",
93
+ page=1,
94
+ page_size=10
95
+ )
96
+ for product in results.products:
97
+ print(f"{product.sku}: {product.name}")
98
+
99
+ # List all products
100
+ results = client.products.list(page=1, page_size=10)
101
+
102
+ # Get warranties for a product
103
+ warranties = client.products.get_warranties(sku=6487435)
104
+
105
+ # Paginate through search results
106
+ for page in client.products.search_pages(query="onSale=true", page_size=100):
107
+ for product in page.products:
108
+ print(product.name)
109
+
110
+ # Iterate over individual products across all pages
111
+ for product in client.products.search_pages(query="onSale=true").items():
112
+ print(product.name)
113
+
114
+ # Limit pagination to a maximum number of pages
115
+ for product in client.products.search_pages(query="onSale=true", max_pages=5).items():
116
+ print(product.name)
117
+ ```
118
+
119
+ #### Categories
120
+
121
+ ```python
122
+ # Get a single category
123
+ category = client.categories.get(category_id="abcat0100000")
124
+ print(f"{category.name}: {category.id}")
125
+
126
+ # Search for categories
127
+ results = client.categories.search(query="name=Laptops*")
128
+ for category in results.categories:
129
+ print(category.name)
130
+
131
+ # List all categories
132
+ results = client.categories.list(page=1, page_size=10)
133
+
134
+ # Paginate through categories
135
+ for category in client.categories.search_pages().items():
136
+ print(category.name)
137
+ ```
138
+
139
+ #### Stores
140
+
141
+ ```python
142
+ # Get a single store by ID
143
+ store = client.stores.get(store_id=281)
144
+ print(f"{store.name}: {store.city}, {store.region}")
145
+
146
+ # Search for stores
147
+ results = client.stores.search(query="city=Minneapolis")
148
+ for store in results.stores:
149
+ print(f"{store.store_id}: {store.name}")
150
+
151
+ # List all stores
152
+ results = client.stores.list(page=1, page_size=10)
153
+
154
+ # Find stores near a ZIP code
155
+ results = client.stores.search_by_area(postal_code="55401", distance=25)
156
+ for store in results.stores:
157
+ print(f"{store.name} - {store.distance} miles")
158
+
159
+ # Find stores near coordinates
160
+ results = client.stores.search_by_area(lat=44.9778, lng=-93.2650, distance=10)
161
+
162
+ # Paginate through stores by area
163
+ for store in client.stores.search_by_area_pages(postal_code="55401").items():
164
+ print(store.name)
165
+ ```
166
+
167
+ #### Recommendations
168
+
169
+ ```python
170
+ # Get trending products
171
+ trending = client.recommendations.trending()
172
+ for product in trending.results:
173
+ print(f"Trending: {product.name}")
174
+
175
+ # Get trending products in a specific category
176
+ trending = client.recommendations.trending(category_id="abcat0502000")
177
+
178
+ # Get most viewed products
179
+ most_viewed = client.recommendations.most_viewed()
180
+ for product in most_viewed.results:
181
+ print(f"Most viewed: {product.name}")
182
+
183
+ # Get products also viewed with a specific product
184
+ also_viewed = client.recommendations.also_viewed(sku=6487435)
185
+ for product in also_viewed.results:
186
+ print(f"Also viewed: {product.name}")
187
+
188
+ # Get products also bought with a specific product
189
+ also_bought = client.recommendations.also_bought(sku=6487435)
190
+
191
+ # Get products ultimately bought after viewing a product
192
+ ultimately_bought = client.recommendations.viewed_ultimately_bought(sku=6487435)
193
+ ```
194
+
195
+ #### Async Catalog Client
196
+
197
+ ```python
198
+ import asyncio
199
+ from bestbuy.clients.catalog import AsyncCatalogClient
200
+
201
+ async def main():
202
+ client = AsyncCatalogClient(api_key="your-api-key")
203
+
204
+ # All operations are async
205
+ product = await client.products.get(sku=6487435)
206
+ print(product.name)
207
+
208
+ # Async pagination
209
+ async for product in client.products.search_pages(query="onSale=true").items():
210
+ print(product.name)
211
+
212
+ asyncio.run(main())
213
+ ```
214
+
215
+ ### Commerce API
216
+
217
+ The Commerce API provides access to Best Buy's order management, fulfillment options, pricing, and payment services.
218
+
219
+ ```python
220
+ from bestbuy.clients.commerce import CommerceClient, AsyncCommerceClient
221
+
222
+ # Initialize the client (sandbox mode by default)
223
+ client = CommerceClient(
224
+ api_key="your-api-key",
225
+ username="your-username", # For registered orders
226
+ password="your-password",
227
+ sandbox=True # Use sandbox environment
228
+ )
229
+
230
+ # Production mode
231
+ client = CommerceClient(
232
+ api_key="your-api-key",
233
+ sandbox=False
234
+ )
235
+ ```
236
+
237
+ #### Authentication
238
+
239
+ Authentication is required for registered orders (orders charged to your company credit card).
240
+
241
+ ```python
242
+ # Using context manager (recommended)
243
+ with client.auth:
244
+ # Session is automatically managed
245
+ response = client.orders.submit_registered(order)
246
+ # Session is automatically logged out
247
+
248
+ # With explicit credentials
249
+ with client.auth(username="user", password="pass"):
250
+ response = client.orders.submit_registered(order)
251
+
252
+ # Manual login/logout
253
+ client.auth.login()
254
+ try:
255
+ response = client.orders.submit_registered(order)
256
+ finally:
257
+ client.auth.logout()
258
+ ```
259
+
260
+ #### Fulfillment Operations
261
+
262
+ ```python
263
+ # Check product availability
264
+ availability = client.fulfillment.check_availability(sku="5628900")
265
+ print(f"Shipping available: {availability.available_for_shipping}")
266
+ print(f"Pickup available: {availability.available_for_pickup}")
267
+ print(f"Delivery available: {availability.available_for_delivery}")
268
+ print(f"Max quantity: {availability.max_quantity}")
269
+
270
+ # Get shipping options
271
+ shipping = client.fulfillment.get_shipping_options(
272
+ sku="5628900",
273
+ address1="123 Main St",
274
+ city="Minneapolis",
275
+ state="MN",
276
+ postalcode="55401"
277
+ )
278
+ for option in shipping.options:
279
+ print(f"{option.name}: ${option.price} - Delivery by {option.expected_delivery_date}")
280
+
281
+ # Find stores with product availability
282
+ stores = client.fulfillment.find_stores(
283
+ sku="5628900",
284
+ zip_code="55401",
285
+ store_count=5
286
+ )
287
+ for store in stores.stores:
288
+ print(f"{store.name}: {store.availability_msg}")
289
+
290
+ # Get home delivery options (for large items)
291
+ delivery_options = client.fulfillment.get_delivery_options(
292
+ sku="5628900",
293
+ zip_code="55401"
294
+ )
295
+ for option in delivery_options.options:
296
+ print(f"{option.delivery_date}: {option.start_time} - {option.end_time}")
297
+
298
+ # Get delivery services (installation, haul away, etc.)
299
+ services = client.fulfillment.get_delivery_services(
300
+ sku="5628900",
301
+ zip_code="55401"
302
+ )
303
+ for service in services.delivery_services:
304
+ print(f"{service.service_display_name}: ${service.price}")
305
+ ```
306
+
307
+ #### Pricing Operations
308
+
309
+ ```python
310
+ # Get unit price for a SKU
311
+ price = client.pricing.get_unit_price(sku="5628900")
312
+ print(f"Price: ${price.unit_price.value}")
313
+
314
+ # Get combined product service info (availability, price, shipping, stores)
315
+ product_info = client.pricing.get_product_service(
316
+ sku="5628900",
317
+ zip_code="55401"
318
+ )
319
+ ```
320
+
321
+ #### Order Operations
322
+
323
+ ```python
324
+ from bestbuy.models.commerce import (
325
+ OrderSubmitRegisteredRequest,
326
+ OrderSubmitGuestRequest,
327
+ OrderList,
328
+ OrderItem,
329
+ Fulfillment,
330
+ AddressFulfillment,
331
+ ShippingAddress,
332
+ Tender,
333
+ )
334
+
335
+ # Create a registered order (requires authentication)
336
+ order = OrderSubmitRegisteredRequest(
337
+ id="my-order-123",
338
+ order_list=OrderList(
339
+ id="list-1",
340
+ items=[
341
+ OrderItem(
342
+ id="item-1",
343
+ quantity=1,
344
+ sku="5628900"
345
+ )
346
+ ]
347
+ ),
348
+ fulfillment=Fulfillment(
349
+ address_fulfillment=AddressFulfillment(
350
+ address=ShippingAddress(
351
+ address1="123 Main St",
352
+ city="Minneapolis",
353
+ state="MN",
354
+ postalcode="55401"
355
+ ),
356
+ shipping_option_key="1" # From get_shipping_options
357
+ )
358
+ ),
359
+ tender=Tender()
360
+ )
361
+
362
+ # Review order (get accurate tax calculation)
363
+ with client.auth:
364
+ review_response = client.orders.review(order)
365
+ print(f"Total: ${review_response.total}")
366
+
367
+ # Submit the order
368
+ submit_response = client.orders.submit_registered(order)
369
+ print(f"Order ID: {submit_response.id_map}")
370
+
371
+ # Query an existing order (no auth required)
372
+ order_details = client.orders.query(
373
+ order_id="BBY01-123456789",
374
+ last_name="Smith",
375
+ phone_number="6125551234"
376
+ )
377
+ print(f"Status: {order_details.status}")
378
+
379
+ # Lookup order by ID (requires auth)
380
+ with client.auth:
381
+ order_details = client.orders.lookup(order_id="BBY01-123456789")
382
+ ```
383
+
384
+ #### Guest Orders
385
+
386
+ Guest orders are charged to the customer's credit card and require payment encryption.
387
+
388
+ ```python
389
+ from bestbuy.utils.encryption import create_encrypted_payment_token
390
+
391
+ # Get encryption key
392
+ encryption_key = client.encryption.get_encryption_key()
393
+
394
+ # Create encrypted payment token
395
+ payment_token = create_encrypted_payment_token(
396
+ card_number="5424180279791773",
397
+ base64_encoded_public_key=encryption_key.base64_encoded_public_key_bytes,
398
+ terminal_id=encryption_key.terminal_id,
399
+ track_id=encryption_key.track_id,
400
+ key_id=encryption_key.key_id
401
+ )
402
+
403
+ # Create guest order with encrypted payment
404
+ guest_order = OrderSubmitGuestRequest(
405
+ id="guest-order-123",
406
+ order_list=OrderList(
407
+ id="list-1",
408
+ items=[
409
+ OrderItem(id="item-1", quantity=1, sku="5628900")
410
+ ]
411
+ ),
412
+ fulfillment=Fulfillment(
413
+ address_fulfillment=AddressFulfillment(
414
+ address=ShippingAddress(
415
+ address1="123 Main St",
416
+ city="Minneapolis",
417
+ state="MN",
418
+ postalcode="55401"
419
+ ),
420
+ shipping_option_key="1"
421
+ )
422
+ ),
423
+ tender=Tender(
424
+ # Include encrypted payment token in tender
425
+ )
426
+ )
427
+
428
+ # Submit guest order (no auth required)
429
+ response = client.orders.submit_guest(guest_order)
430
+ ```
431
+
432
+ #### Encryption Operations
433
+
434
+ ```python
435
+ # Get the public encryption key for guest orders
436
+ key_response = client.encryption.get_encryption_key()
437
+ print(f"Terminal ID: {key_response.terminal_id}")
438
+ print(f"Track ID: {key_response.track_id}")
439
+ print(f"Key ID: {key_response.key_id}")
440
+ ```
441
+
442
+ #### Async Commerce Client
443
+
444
+ ```python
445
+ import asyncio
446
+ from bestbuy.clients.commerce import AsyncCommerceClient
447
+
448
+ async def main():
449
+ client = AsyncCommerceClient(
450
+ api_key="your-api-key",
451
+ username="your-username",
452
+ password="your-password"
453
+ )
454
+
455
+ # Check availability
456
+ availability = await client.fulfillment.check_availability(sku="5628900")
457
+ print(availability.available_for_shipping)
458
+
459
+ # Async authentication context
460
+ async with client.auth:
461
+ response = await client.orders.submit_registered(order)
462
+
463
+ asyncio.run(main())
464
+ ```
465
+
466
+ ### Configuration Options
467
+
468
+ #### Catalog Client Options
469
+
470
+ | Option | Type | Default | Description |
471
+ |--------|------|---------|-------------|
472
+ | `api_key` | str | Required | Best Buy API key |
473
+ | `base_url` | str | `https://api.bestbuy.com` | API base URL |
474
+ | `timeout_ms` | int | `30000` | Request timeout in milliseconds |
475
+ | `log_level` | int | `logging.WARNING` | Logging level |
476
+
477
+ #### Commerce Client Options
478
+
479
+ | Option | Type | Default | Description |
480
+ |--------|------|---------|-------------|
481
+ | `api_key` | str | Required | Best Buy API key |
482
+ | `username` | str | None | Billing account username |
483
+ | `password` | str | None | Billing account password |
484
+ | `partner_id` | str | None | Partner ID for orders |
485
+ | `sandbox` | bool | `True` | Use sandbox environment |
486
+ | `auto_logout` | bool | `True` | Auto-logout on session end |
487
+ | `base_url` | str | Auto | API base URL (auto-set based on sandbox) |
488
+ | `timeout_ms` | int | `30000` | Request timeout in milliseconds |
489
+ | `log_level` | int | `logging.WARNING` | Logging level |
490
+
491
+ ### Error Handling
492
+
493
+ ```python
494
+ from bestbuy.exceptions import APIError, ConfigError, SessionRequiredError
495
+
496
+ try:
497
+ product = client.products.get(sku=999999999)
498
+ except APIError as e:
499
+ print(f"API Error: {e.message}")
500
+ print(f"Error Code: {e.code}")
501
+ print(f"Response: {e.response_text}")
502
+ except ConfigError as e:
503
+ print(f"Configuration Error: {e}")
504
+
505
+ # Commerce API specific
506
+ try:
507
+ client.orders.submit_registered(order) # Without authentication
508
+ except SessionRequiredError as e:
509
+ print("Must be logged in to submit orders")
510
+ ```
511
+
512
+ ### Sandbox Testing
513
+
514
+ For the Commerce API, use the sandbox environment for testing:
515
+
516
+ ```python
517
+ client = CommerceClient(
518
+ api_key="your-sandbox-api-key",
519
+ sandbox=True # Default
520
+ )
521
+
522
+ # Test credit card for sandbox
523
+ # Card Number: 5424180279791773
524
+ # Expiration: 12/2025
525
+ # CVV: 999
526
+ ```
527
+
528
+ ## Development
529
+
530
+ ### Running Tests
531
+
532
+ ```bash
533
+ uv run pytest
534
+ ```
535
+
536
+ ### Running Type Checks
537
+
538
+ ```bash
539
+ uv run mypy
540
+ ```
541
+
542
+ ## License
543
+
544
+ MIT