otterapi 0.0.4__tar.gz → 0.0.6__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 (59) hide show
  1. {otterapi-0.0.4 → otterapi-0.0.6}/.gitignore +3 -0
  2. otterapi-0.0.6/PKG-INFO +627 -0
  3. otterapi-0.0.6/README.md +599 -0
  4. otterapi-0.0.6/otterapi/__init__.py +73 -0
  5. otterapi-0.0.6/otterapi/cli.py +382 -0
  6. otterapi-0.0.6/otterapi/codegen/__init__.py +115 -0
  7. otterapi-0.0.6/otterapi/codegen/ast_utils.py +250 -0
  8. otterapi-0.0.6/otterapi/codegen/client.py +1271 -0
  9. otterapi-0.0.6/otterapi/codegen/codegen.py +1736 -0
  10. otterapi-0.0.6/otterapi/codegen/dataframes.py +392 -0
  11. otterapi-0.0.6/otterapi/codegen/emitter.py +473 -0
  12. otterapi-0.0.6/otterapi/codegen/endpoints.py +2755 -0
  13. otterapi-0.0.6/otterapi/codegen/pagination.py +1026 -0
  14. otterapi-0.0.6/otterapi/codegen/schema.py +593 -0
  15. otterapi-0.0.6/otterapi/codegen/splitting.py +1397 -0
  16. otterapi-0.0.6/otterapi/codegen/types.py +1345 -0
  17. otterapi-0.0.6/otterapi/codegen/utils.py +261 -0
  18. otterapi-0.0.6/otterapi/config.py +1067 -0
  19. otterapi-0.0.6/otterapi/exceptions.py +231 -0
  20. otterapi-0.0.6/otterapi/openapi/__init__.py +46 -0
  21. otterapi-0.0.6/otterapi/openapi/v2/__init__.py +86 -0
  22. otterapi-0.0.6/otterapi/openapi/v2/spec.json +1607 -0
  23. otterapi-0.0.6/otterapi/openapi/v2/v2.py +1776 -0
  24. otterapi-0.0.6/otterapi/openapi/v3/__init__.py +131 -0
  25. otterapi-0.0.6/otterapi/openapi/v3/spec.json +1651 -0
  26. otterapi-0.0.6/otterapi/openapi/v3/v3.py +1557 -0
  27. otterapi-0.0.6/otterapi/openapi/v3_1/__init__.py +133 -0
  28. otterapi-0.0.6/otterapi/openapi/v3_1/spec.json +1411 -0
  29. otterapi-0.0.6/otterapi/openapi/v3_1/v3_1.py +798 -0
  30. otterapi-0.0.6/otterapi/openapi/v3_2/__init__.py +133 -0
  31. otterapi-0.0.6/otterapi/openapi/v3_2/spec.json +1666 -0
  32. otterapi-0.0.6/otterapi/openapi/v3_2/v3_2.py +777 -0
  33. otterapi-0.0.6/otterapi/tests/__init__.py +3 -0
  34. otterapi-0.0.6/otterapi/tests/fixtures/__init__.py +455 -0
  35. otterapi-0.0.6/otterapi/tests/test_ast_utils.py +680 -0
  36. otterapi-0.0.6/otterapi/tests/test_codegen.py +610 -0
  37. otterapi-0.0.6/otterapi/tests/test_dataframe.py +1038 -0
  38. otterapi-0.0.6/otterapi/tests/test_exceptions.py +493 -0
  39. otterapi-0.0.6/otterapi/tests/test_openapi_support.py +616 -0
  40. otterapi-0.0.6/otterapi/tests/test_openapi_upgrade.py +215 -0
  41. otterapi-0.0.6/otterapi/tests/test_pagination.py +1101 -0
  42. otterapi-0.0.6/otterapi/tests/test_splitting_config.py +319 -0
  43. otterapi-0.0.6/otterapi/tests/test_splitting_integration.py +427 -0
  44. otterapi-0.0.6/otterapi/tests/test_splitting_resolver.py +512 -0
  45. otterapi-0.0.6/otterapi/tests/test_splitting_tree.py +525 -0
  46. {otterapi-0.0.4 → otterapi-0.0.6}/pyproject.toml +12 -11
  47. otterapi-0.0.4/PKG-INFO +0 -54
  48. otterapi-0.0.4/README.md +0 -26
  49. otterapi-0.0.4/otterapi/__init__.py +0 -0
  50. otterapi-0.0.4/otterapi/cli.py +0 -84
  51. otterapi-0.0.4/otterapi/codegen/__init__.py +0 -0
  52. otterapi-0.0.4/otterapi/codegen/ast_utils.py +0 -118
  53. otterapi-0.0.4/otterapi/codegen/endpoints.py +0 -499
  54. otterapi-0.0.4/otterapi/codegen/generator.py +0 -352
  55. otterapi-0.0.4/otterapi/codegen/openapi_processor.py +0 -27
  56. otterapi-0.0.4/otterapi/codegen/type_generator.py +0 -538
  57. otterapi-0.0.4/otterapi/codegen/utils.py +0 -55
  58. otterapi-0.0.4/otterapi/config.py +0 -74
  59. {otterapi-0.0.4 → otterapi-0.0.6}/otterapi/__main__.py +0 -0
@@ -818,3 +818,6 @@ fabric.properties
818
818
  # Android studio 3.1+ serialized cache file
819
819
  .idea/caches/build_file_checksums.ser
820
820
 
821
+
822
+ otter.yaml
823
+ otter.yml
@@ -0,0 +1,627 @@
1
+ Metadata-Version: 2.4
2
+ Name: otterapi
3
+ Version: 0.0.6
4
+ Summary: A cute little companion that generates type-safe clients from OpenAPI documents.
5
+ Project-URL: Source, https://github.com/danplischke/otter
6
+ Author: Dan Plischke
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Operating System :: MacOS
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Operating System :: POSIX
12
+ Classifier: Operating System :: Unix
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx~=0.28.1
22
+ Requires-Dist: pydantic-settings~=2.11.0
23
+ Requires-Dist: pydantic[email]~=2.12.3
24
+ Requires-Dist: pyyaml~=6.0.3
25
+ Requires-Dist: typer~=0.20.0
26
+ Requires-Dist: universal-pathlib~=0.3.4
27
+ Description-Content-Type: text/markdown
28
+
29
+ # 🦦 OtterAPI
30
+
31
+ > *A cute and intelligent OpenAPI client generator that dives deep into your OpenAPIs*
32
+
33
+ **OtterAPI** is a sleek Python library that transforms OpenAPI specifications into clean, type-safe client code with Pydantic models and httpx-based HTTP clients.
34
+
35
+ ## ✨ Features
36
+
37
+ - **Type-Safe Code Generation** - Generates Pydantic models and fully typed endpoint functions
38
+ - **Sync & Async Support** - Generate both synchronous and asynchronous API clients
39
+ - **OpenAPI 3.x Support** - Full support for OpenAPI 3.0, 3.1, and 3.2 specifications
40
+ - **Module Splitting** - Organize large APIs into multiple organized files
41
+ - **Customizable Client** - Generated client class with configurable base URL, timeout, and headers
42
+ - **Environment Variable Support** - Use `${VAR}` or `${VAR:-default}` syntax in config files
43
+
44
+ ## 🚀 Quick Start
45
+
46
+ ### Installation
47
+
48
+ ```bash
49
+ pip install otterapi
50
+ ```
51
+
52
+ ### Basic Usage
53
+
54
+ 1. Create an `otter.yml` configuration file:
55
+
56
+ ```yaml
57
+ documents:
58
+ - source: https://petstore3.swagger.io/api/v3/openapi.json
59
+ output: petstore_client
60
+ ```
61
+
62
+ 2. Generate the client:
63
+
64
+ ```bash
65
+ otter generate
66
+ ```
67
+
68
+ 3. Use the generated code:
69
+
70
+ ```python
71
+ from petstore_client import get_pet_by_id, aget_pet_by_id
72
+
73
+ # Synchronous usage
74
+ pet = get_pet_by_id(pet_id=123)
75
+
76
+ # Asynchronous usage
77
+ import asyncio
78
+ pet = asyncio.run(aget_pet_by_id(pet_id=123))
79
+ ```
80
+
81
+ ## 📝 Configuration
82
+
83
+ ### Basic Configuration
84
+
85
+ ```yaml
86
+ documents:
87
+ - source: https://petstore3.swagger.io/api/v3/openapi.json
88
+ output: petstore_client
89
+
90
+ - source: ./local-api.json
91
+ output: local_client
92
+ base_url: https://api.example.com
93
+ ```
94
+
95
+ ### Full Configuration Options
96
+
97
+ ```yaml
98
+ documents:
99
+ - source: https://api.example.com/openapi.json # URL or file path (required)
100
+ output: ./client # Output directory (required)
101
+ base_url: https://api.example.com # Override base URL from spec
102
+ models_file: models.py # Models filename (default: models.py)
103
+ endpoints_file: endpoints.py # Endpoints filename (default: endpoints.py)
104
+ generate_async: true # Generate async functions (default: true)
105
+ generate_sync: true # Generate sync functions (default: true)
106
+ client_class_name: MyAPIClient # Client class name (default: from API title)
107
+ module_split: # Module splitting configuration
108
+ enabled: false # Enable splitting (default: false)
109
+ # ... see Module Splitting section below
110
+ ```
111
+
112
+ ---
113
+
114
+ ## 📦 Module Splitting
115
+
116
+ For large APIs with many endpoints, OtterAPI can split the generated code into multiple organized modules instead of a single `endpoints.py` file.
117
+
118
+ ### Why Use Module Splitting?
119
+
120
+ - **Better Organization** - Group related endpoints together
121
+ - **Easier Navigation** - Find endpoints quickly in smaller files
122
+ - **Improved IDE Performance** - Smaller files load faster
123
+ - **Cleaner Imports** - Import only what you need from specific modules
124
+
125
+ ### Enabling Module Splitting
126
+
127
+ ```yaml
128
+ documents:
129
+ - source: https://api.example.com/openapi.json
130
+ output: ./client
131
+ module_split:
132
+ enabled: true
133
+ strategy: tag
134
+ ```
135
+
136
+ ### Splitting Strategies
137
+
138
+ #### `tag` - Split by OpenAPI Tags
139
+
140
+ Uses the first tag from each operation to determine the module:
141
+
142
+ ```yaml
143
+ module_split:
144
+ enabled: true
145
+ strategy: tag
146
+ min_endpoints: 1
147
+ ```
148
+
149
+ **Result:** Endpoints tagged with `["Users"]` go to `users.py`, `["Orders"]` go to `orders.py`, etc.
150
+
151
+ #### `path` - Split by URL Path
152
+
153
+ Uses the first segment(s) of the URL path:
154
+
155
+ ```yaml
156
+ module_split:
157
+ enabled: true
158
+ strategy: path
159
+ path_depth: 1 # Number of path segments to use
160
+ global_strip_prefixes: # Remove these prefixes first
161
+ - /api/v1
162
+ - /api/v2
163
+ ```
164
+
165
+ **Result:** `/api/v1/users/123` → `users.py`, `/api/v1/orders/456` → `orders.py`
166
+
167
+ #### `custom` - Explicit Module Mapping
168
+
169
+ Define exactly which paths go to which modules using glob patterns:
170
+
171
+ ```yaml
172
+ module_split:
173
+ enabled: true
174
+ strategy: custom
175
+ module_map:
176
+ users:
177
+ - /users
178
+ - /users/*
179
+ - /users/**
180
+ orders:
181
+ - /orders/*
182
+ - /orders/**
183
+ health:
184
+ - /health
185
+ - /ready
186
+ - /live
187
+ ```
188
+
189
+ #### `hybrid` - Combined Strategy (Default)
190
+
191
+ Tries custom module_map first, then falls back to tags, then path:
192
+
193
+ ```yaml
194
+ module_split:
195
+ enabled: true
196
+ strategy: hybrid
197
+ module_map:
198
+ health: # Custom mapping takes priority
199
+ - /health
200
+ - /ready
201
+ # Remaining endpoints use tags if available, otherwise path
202
+ ```
203
+
204
+ #### `none` - All to Fallback
205
+
206
+ All endpoints go to a single fallback module:
207
+
208
+ ```yaml
209
+ module_split:
210
+ enabled: true
211
+ strategy: none
212
+ fallback_module: api # All endpoints go here
213
+ ```
214
+
215
+ ### Pattern Syntax
216
+
217
+ The module map supports glob patterns:
218
+
219
+ | Pattern | Matches | Example |
220
+ |---------|---------|---------|
221
+ | `/users` | Exact path | `/users` only |
222
+ | `/users/*` | Single segment | `/users/123`, `/users/abc` |
223
+ | `/users/**` | Any depth | `/users/123`, `/users/123/profile/settings` |
224
+ | `/v?/users` | Single character | `/v1/users`, `/v2/users` |
225
+
226
+ ### Nested Module Maps
227
+
228
+ Create hierarchical module structures:
229
+
230
+ ```yaml
231
+ module_split:
232
+ enabled: true
233
+ strategy: custom
234
+ module_map:
235
+ identity: # Parent module
236
+ users: # Child: identity/users.py
237
+ - /users/*
238
+ - /users/**
239
+ auth: # Child: identity/auth.py
240
+ - /auth/*
241
+ - /login
242
+ - /logout
243
+ roles: # Child: identity/roles.py
244
+ - /roles/*
245
+ billing:
246
+ invoices:
247
+ - /invoices/*
248
+ payments:
249
+ - /payments/*
250
+ ```
251
+
252
+ ### Advanced Module Definition
253
+
254
+ Full control over each module:
255
+
256
+ ```yaml
257
+ module_split:
258
+ enabled: true
259
+ strategy: custom
260
+ module_map:
261
+ v2_api:
262
+ paths: # Explicit paths key
263
+ - /v2/**
264
+ strip_prefix: /v2 # Strip this prefix from paths in this module
265
+ description: "API v2 endpoints (deprecated)" # Module docstring
266
+ modules: # Nested submodules
267
+ users:
268
+ paths:
269
+ - /users/*
270
+ billing:
271
+ paths:
272
+ - /billing/*
273
+ ```
274
+
275
+ ### Module Split Options Reference
276
+
277
+ | Option | Type | Default | Description |
278
+ |--------|------|---------|-------------|
279
+ | `enabled` | bool | `false` | Enable module splitting |
280
+ | `strategy` | string | `hybrid` | Strategy: `none`, `path`, `tag`, `hybrid`, `custom` |
281
+ | `fallback_module` | string | `common` | Module name for unmatched endpoints |
282
+ | `min_endpoints` | int | `2` | Minimum endpoints per module (smaller modules get consolidated) |
283
+ | `flat_structure` | bool | `false` | `true`: flat files, `false`: nested directories |
284
+ | `path_depth` | int | `1` | Path segments to use for `path` strategy (1-5) |
285
+ | `global_strip_prefixes` | list | common prefixes | Prefixes to strip from all paths before matching |
286
+ | `module_map` | object | `{}` | Custom module mappings |
287
+
288
+ ### Output Structure Examples
289
+
290
+ **Flat Structure (default):**
291
+
292
+ ```
293
+ client/
294
+ ├── __init__.py # Re-exports all endpoints
295
+ ├── models.py # Pydantic models
296
+ ├── _client.py # Base client class
297
+ ├── client.py # User-customizable client
298
+ ├── users.py # User endpoints
299
+ ├── orders.py # Order endpoints
300
+ └── health.py # Health check endpoints
301
+ ```
302
+
303
+ **Nested Structure** (`flat_structure: false` with nested module_map):
304
+
305
+ ```
306
+ client/
307
+ ├── __init__.py
308
+ ├── models.py
309
+ ├── _client.py
310
+ ├── client.py
311
+ ├── identity/
312
+ │ ├── __init__.py
313
+ │ ├── users.py
314
+ │ └── auth.py
315
+ └── billing/
316
+ ├── __init__.py
317
+ └── invoices.py
318
+ ```
319
+
320
+ ### Complete Example
321
+
322
+ ```yaml
323
+ documents:
324
+ - source: https://api.mycompany.com/openapi.json
325
+ output: ./mycompany_client
326
+ module_split:
327
+ enabled: true
328
+ strategy: custom
329
+
330
+ # Strip API version prefixes
331
+ global_strip_prefixes:
332
+ - /api/v1
333
+ - /api/v2
334
+ - /api/v3
335
+
336
+ # Consolidate small modules (< 3 endpoints) into fallback
337
+ min_endpoints: 3
338
+ fallback_module: misc
339
+
340
+ # Custom module organization
341
+ module_map:
342
+ # Simple health checks
343
+ health:
344
+ - /health
345
+ - /ready
346
+ - /metrics
347
+
348
+ # User management
349
+ users:
350
+ - /users
351
+ - /users/*
352
+ - /users/**
353
+
354
+ # Authentication
355
+ auth:
356
+ - /auth/*
357
+ - /login
358
+ - /logout
359
+ - /refresh
360
+
361
+ # Nested billing module
362
+ billing:
363
+ paths:
364
+ - /billing/**
365
+ description: "Billing and payment endpoints"
366
+ modules:
367
+ invoices:
368
+ - /invoices/*
369
+ subscriptions:
370
+ - /subscriptions/*
371
+ payments:
372
+ - /payments/*
373
+ ```
374
+
375
+ ---
376
+
377
+ ## 📊 DataFrame Conversion
378
+
379
+ OtterAPI can generate additional methods that return pandas or polars DataFrames directly, making it easy to analyze API responses.
380
+
381
+ ### Enabling DataFrame Methods
382
+
383
+ ```yaml
384
+ documents:
385
+ - source: https://api.example.com/openapi.json
386
+ output: ./client
387
+ dataframe:
388
+ enabled: true
389
+ pandas: true # Generate _df methods (default: true when enabled)
390
+ polars: true # Generate _pl methods (default: false)
391
+ ```
392
+
393
+ ### Generated Methods
394
+
395
+ When enabled, endpoints that return lists get additional DataFrame methods:
396
+
397
+ | Original Method | Pandas Method | Polars Method |
398
+ |-----------------|---------------|---------------|
399
+ | `get_users()` | `get_users_df()` | `get_users_pl()` |
400
+ | `aget_users()` | `aget_users_df()` | `aget_users_pl()` |
401
+
402
+ ### Basic Usage
403
+
404
+ ```python
405
+ from client import find_pets_by_status, find_pets_by_status_df, find_pets_by_status_pl
406
+
407
+ # Get as Pydantic models (existing behavior)
408
+ pets = find_pets_by_status("available")
409
+ for pet in pets:
410
+ print(f"{pet.id}: {pet.name}")
411
+
412
+ # Get as pandas DataFrame
413
+ pdf = find_pets_by_status_df("available")
414
+ print(pdf.head())
415
+ print(pdf.describe())
416
+
417
+ # Get as polars DataFrame
418
+ plf = find_pets_by_status_pl("available")
419
+ print(plf.schema)
420
+ print(plf.head())
421
+ ```
422
+
423
+ ### Handling Nested Responses
424
+
425
+ For APIs that return data nested under a key (e.g., `{"data": {"users": [...]}}`):
426
+
427
+ ```yaml
428
+ dataframe:
429
+ enabled: true
430
+ pandas: true
431
+ polars: true
432
+ default_path: "data.items" # Default path for all endpoints
433
+ endpoints:
434
+ get_users:
435
+ path: "data.users" # Override for specific endpoint
436
+ get_analytics:
437
+ path: "response.events"
438
+ ```
439
+
440
+ You can also override the path at runtime:
441
+
442
+ ```python
443
+ # Use configured path
444
+ df = get_users_df()
445
+
446
+ # Override path at call time
447
+ df = get_users_df(path="response.data.users")
448
+ ```
449
+
450
+ ### Selective Generation
451
+
452
+ Control which endpoints get DataFrame methods:
453
+
454
+ ```yaml
455
+ dataframe:
456
+ enabled: true
457
+ pandas: true
458
+ polars: true
459
+ include_all: false # Don't generate for all endpoints
460
+ endpoints:
461
+ list_users:
462
+ enabled: true # Only generate for this endpoint
463
+ get_analytics:
464
+ enabled: true
465
+ path: "events"
466
+ polars: true
467
+ pandas: false # Only polars for this endpoint
468
+ ```
469
+
470
+ ### DataFrame Configuration Options
471
+
472
+ | Option | Type | Default | Description |
473
+ |--------|------|---------|-------------|
474
+ | `enabled` | bool | `false` | Enable DataFrame method generation |
475
+ | `pandas` | bool | `true` | Generate `_df` methods (pandas) |
476
+ | `polars` | bool | `false` | Generate `_pl` methods (polars) |
477
+ | `default_path` | string | `null` | Default JSON path for extracting data |
478
+ | `include_all` | bool | `true` | Generate for all list-returning endpoints |
479
+ | `endpoints` | object | `{}` | Per-endpoint configuration overrides |
480
+
481
+ ### Per-Endpoint Options
482
+
483
+ | Option | Type | Default | Description |
484
+ |--------|------|---------|-------------|
485
+ | `enabled` | bool | inherits | Override whether to generate methods |
486
+ | `path` | string | inherits | JSON path to extract data |
487
+ | `pandas` | bool | inherits | Override pandas generation |
488
+ | `polars` | bool | inherits | Override polars generation |
489
+
490
+ ---
491
+
492
+ ## 📖 Using Generated Code
493
+
494
+ ### Direct Function Imports
495
+
496
+ ```python
497
+ # Import specific endpoints
498
+ from client import get_user, create_user, list_orders
499
+
500
+ # Async versions are prefixed with 'a'
501
+ from client import aget_user, acreate_user, alist_orders
502
+
503
+ # Sync usage
504
+ user = get_user(user_id=123)
505
+ orders = list_orders(status="pending", limit=10)
506
+
507
+ # Async usage
508
+ import asyncio
509
+
510
+ async def main():
511
+ user = await aget_user(user_id=123)
512
+ orders = await alist_orders(status="pending")
513
+
514
+ asyncio.run(main())
515
+ ```
516
+
517
+ ### Module-Specific Imports (with splitting)
518
+
519
+ ```python
520
+ # Import from specific modules
521
+ from client.users import get_user, create_user
522
+ from client.orders import list_orders, get_order
523
+ from client.auth import login, logout
524
+ ```
525
+
526
+ ### Using the Client Class
527
+
528
+ ```python
529
+ from client import Client
530
+
531
+ # Create client with default settings
532
+ client = Client()
533
+
534
+ # Or customize the client
535
+ client = Client(
536
+ base_url="https://api.example.com",
537
+ timeout=30.0,
538
+ headers={
539
+ "Authorization": "Bearer your-token",
540
+ "X-Custom-Header": "value"
541
+ }
542
+ )
543
+
544
+ # Use client methods (sync)
545
+ user = client.get_user(user_id=123)
546
+ orders = client.list_orders(status="pending")
547
+
548
+ # Use async methods
549
+ import asyncio
550
+
551
+ async def main():
552
+ user = await client.aget_user(user_id=123)
553
+
554
+ asyncio.run(main())
555
+ ```
556
+
557
+ ### Working with Models
558
+
559
+ ```python
560
+ from client.models import User, Order, CreateUserRequest
561
+
562
+ # Models are Pydantic BaseModels
563
+ new_user = CreateUserRequest(
564
+ name="John Doe",
565
+ email="john@example.com"
566
+ )
567
+
568
+ # Create user
569
+ user = create_user(body=new_user)
570
+
571
+ # Access typed response
572
+ print(user.id)
573
+ print(user.name)
574
+ print(user.email)
575
+ ```
576
+
577
+ ---
578
+
579
+ ## 🔧 CLI Reference
580
+
581
+ ```bash
582
+ # Generate from default config (otter.yml, otter.yaml, or pyproject.toml)
583
+ otter generate
584
+
585
+ # Generate from specific config file
586
+ otter generate -c my-config.yml
587
+
588
+ # Initialize a new config file
589
+ otter init
590
+
591
+ # Validate configuration
592
+ otter validate
593
+ ```
594
+
595
+ ---
596
+
597
+ ## 🛠 Development
598
+
599
+ ```bash
600
+ # Clone the repository
601
+ git clone https://github.com/yourusername/otterapi.git
602
+ cd otterapi
603
+
604
+ # Install dependencies with uv
605
+ uv sync
606
+
607
+ # Run tests
608
+ uv run pytest
609
+
610
+ # Run tests with coverage
611
+ uv run pytest --cov=otterapi
612
+
613
+ # Run the generator
614
+ uv run python -m otterapi generate
615
+
616
+ # Format code
617
+ uv run ruff format .
618
+
619
+ # Lint code
620
+ uv run ruff check .
621
+ ```
622
+
623
+ ---
624
+
625
+ ## 📄 License
626
+
627
+ MIT License - see LICENSE for details.