strawberry-orm 0.2.0__tar.gz → 0.4.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 (24) hide show
  1. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/PKG-INFO +242 -4
  2. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/README.md +240 -3
  3. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/pyproject.toml +3 -1
  4. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/_base.py +126 -21
  5. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/django.py +44 -8
  6. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/protocol.py +1 -0
  7. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/sqlalchemy.py +51 -11
  8. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/tortoise.py +204 -36
  9. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/core.py +35 -4
  10. strawberry_orm-0.4.0/src/strawberry_orm/mutations.py +1344 -0
  11. strawberry_orm-0.2.0/src/strawberry_orm/mutations.py +0 -59
  12. strawberry_orm-0.2.0/src/strawberry_orm/relay/resolvers.py +0 -119
  13. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/__init__.py +0 -0
  14. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/_async.py +0 -0
  15. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/backends/__init__.py +0 -0
  16. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/fields.py +0 -0
  17. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/filters.py +0 -0
  18. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/optimizer/__init__.py +0 -0
  19. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/optimizer/extension.py +0 -0
  20. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/optimizer/store.py +0 -0
  21. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/py.typed +0 -0
  22. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/relay/__init__.py +0 -0
  23. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/relay/connection.py +0 -0
  24. {strawberry_orm-0.2.0 → strawberry_orm-0.4.0}/src/strawberry_orm/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strawberry-orm
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Unified, backend-agnostic ORM abstraction for Strawberry GraphQL
5
5
  Author: James Davidson, Patrick Arminio
6
6
  Author-email: James Davidson <jamie.t.davidson@gmail.com>, Patrick Arminio <patrick.arminio@gmail.com>
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Programming Language :: Python :: 3.14
15
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
16
  Requires-Dist: strawberry-graphql>=0.311.0
17
+ Requires-Dist: greenlet>=3.3.2 ; extra == 'dev'
17
18
  Requires-Dist: pytest>=8.0 ; extra == 'dev'
18
19
  Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
19
20
  Requires-Dist: pytest-django>=4.8 ; extra == 'dev'
@@ -54,9 +55,9 @@ Backend-agnostic schema generation for [Strawberry GraphQL](https://strawberry.r
54
55
  uv add strawberry-orm
55
56
 
56
57
  # With a backend
57
- uv add strawberry-orm[django]
58
- uv add strawberry-orm[sqlalchemy]
59
- uv add strawberry-orm[tortoise]
58
+ uv add "strawberry-orm[django]"
59
+ uv add "strawberry-orm[sqlalchemy]"
60
+ uv add "strawberry-orm[tortoise]"
60
61
  ```
61
62
 
62
63
  You can do the same with `pip`:
@@ -65,6 +66,8 @@ You can do the same with `pip`:
65
66
  pip install "strawberry-orm[sqlalchemy]"
66
67
  ```
67
68
 
69
+ If you are using `zsh`, keep the quotes around extras such as `"strawberry-orm[sqlalchemy]"`. Unquoted square brackets are treated as shell glob syntax before `uv` or `pip` sees the package name.
70
+
68
71
  Requirements:
69
72
 
70
73
  - Python `>=3.12`
@@ -422,6 +425,104 @@ Typical string lookups: `exact`, `neq`, `contains`, `iContains`, `startsWith`, `
422
425
 
423
426
  Regex lookups (`regex`, `iRegex`) are disabled by default. Enable with `enable_regex_filters=True`.
424
427
 
428
+ #### Object Traversal
429
+
430
+ When filters are registered for related models, the generated filter gains an `object` key that lets you filter parent rows based on conditions on their related objects:
431
+
432
+ ```python
433
+ UserFilter = orm.filter(User)
434
+ PostFilter = orm.filter(Post) # Post has an "author" relation to User
435
+ ```
436
+
437
+ ```graphql
438
+ {
439
+ posts(filter: {
440
+ object: { author: { field: { name: { exact: "Alice" } } } }
441
+ }) {
442
+ title
443
+ }
444
+ }
445
+ ```
446
+
447
+ Object traversal composes with boolean operators:
448
+
449
+ ```graphql
450
+ {
451
+ posts(filter: {
452
+ all: [
453
+ { field: { isPublished: { exact: true } } }
454
+ { object: { author: { field: { name: { exact: "Alice" } } } } }
455
+ ]
456
+ }) { title }
457
+ }
458
+ ```
459
+
460
+ Multi-level traversal works when the intermediate models also have registered filters:
461
+
462
+ ```python
463
+ UserFilter = orm.filter(User)
464
+ PostFilter = orm.filter(Post)
465
+ CommentFilter = orm.filter(Comment) # Comment -> Post -> User
466
+ ```
467
+
468
+ ```graphql
469
+ # Find comments on posts written by Alice
470
+ {
471
+ comments(filter: {
472
+ object: { post: {
473
+ object: { author: { field: { name: { exact: "Alice" } } } }
474
+ } }
475
+ }) { body }
476
+ }
477
+ ```
478
+
479
+ The `object` type is `@oneOf`, so each filter entry names exactly one relation.
480
+
481
+ Relations only appear in `object` if their target model already has a registered filter at the time `orm.filter()` is called. Register child model filters before parent model filters.
482
+
483
+ #### Filter Projection
484
+
485
+ By default every relation with a registered filter appears in `object`. Pass `project={...}` to `orm.filter()` to control exactly which relations are exposed and how deep traversal can go:
486
+
487
+ ```python
488
+ UserFilter = orm.filter(User)
489
+ TagFilter = orm.filter(Tag)
490
+ CommentFilter = orm.filter(Comment)
491
+
492
+ # Only expose "author" in the object type — tags and comments are excluded
493
+ PostFilter = orm.filter(Post, project={"author": {}})
494
+ ```
495
+
496
+ ```graphql
497
+ # This works (author is projected)
498
+ { posts(filter: { object: { author: { field: { name: { exact: "Alice" } } } } }) { title } }
499
+
500
+ # This would be a schema error (tags is not projected)
501
+ { posts(filter: { object: { tags: { field: { name: { exact: "python" } } } } }) { title } }
502
+ ```
503
+
504
+ Sub-project dicts control nested traversal. An empty dict `{}` means "include this relation but don't allow further object traversal from it". A non-empty dict lists which of the related model's relations are reachable:
505
+
506
+ ```python
507
+ # Allow Comment -> post, and from post -> author (but not post -> tags)
508
+ CommentFilter = orm.filter(Comment, project={
509
+ "post": {
510
+ "author": {},
511
+ },
512
+ })
513
+ ```
514
+
515
+ Summary:
516
+
517
+ | `project` value | Behavior |
518
+ | --- | --- |
519
+ | `None` (default) | Auto-include all relations with registered filters |
520
+ | `{}` | No `object` type at all (scalar lookups only) |
521
+ | `{"rel": {}}` | Include `rel` as a leaf (no further traversal from it) |
522
+ | `{"rel": {"nested": {}}}` | Include `rel` and allow traversal to `nested` from it |
523
+
524
+ Projected filters are cached internally and do not overwrite the global filter registry, so you can create multiple projected variants of the same model's filter for different schema entry points.
525
+
425
526
  ### Ordering
426
527
 
427
528
  Generate an order input:
@@ -640,6 +741,143 @@ In practice:
640
741
 
641
742
  ---
642
743
 
744
+ ### Recursive Node Mutations
745
+
746
+ `orm.mutations.create_node(...)` and `orm.mutations.update_node(...)` generate catch-all Relay `Node` mutations with recursive nested inputs.
747
+
748
+ If you want to implement the resolver logic yourself, you can generate the root input types directly with `orm.mutations.create_node_input(...)` and `orm.mutations.update_node_input(...)`:
749
+
750
+ ```python
751
+ import strawberry
752
+ from strawberry import relay
753
+
754
+
755
+ @orm.type(User)
756
+ class UserNode(relay.Node):
757
+ id: relay.NodeID[int]
758
+ name: auto
759
+ email: auto
760
+
761
+
762
+ @orm.type(Post)
763
+ class PostNode(relay.Node):
764
+ id: relay.NodeID[int]
765
+ title: auto
766
+ body: auto
767
+
768
+
769
+ CreateNodeInput = orm.mutations.create_node_input()
770
+ UpdateNodeInput = orm.mutations.update_node_input()
771
+
772
+
773
+ @strawberry.type
774
+ class Mutation:
775
+ create_node = orm.mutations.create_node()
776
+ update_node = orm.mutations.update_node()
777
+
778
+ @strawberry.field
779
+ def custom_create_node(self, input: CreateNodeInput) -> str:
780
+ return "implement your own create logic here"
781
+
782
+ @strawberry.field
783
+ def custom_update_node(self, input: UpdateNodeInput) -> str:
784
+ return "implement your own update logic here"
785
+ ```
786
+
787
+ List relations use `items`, while singular relations use explicit `create` / `update` branches:
788
+
789
+ ```graphql
790
+ mutation {
791
+ createNode(input: {
792
+ post: {
793
+ title: "Hello"
794
+ body: "World"
795
+ author: {
796
+ create: {
797
+ name: "Alice"
798
+ email: "alice@example.com"
799
+ }
800
+ }
801
+ tags: {
802
+ items: [{ create: { name: "python" } }]
803
+ mode: REPLACE
804
+ onRemove: DELETE
805
+ }
806
+ }
807
+ }) {
808
+ __typename
809
+ }
810
+ }
811
+ ```
812
+
813
+ #### Projection And Policy Config
814
+
815
+ Pass `project={...}` to restrict recursion depth and configure relation semantics in one dict:
816
+
817
+ ```python
818
+ project = {
819
+ "post": {
820
+ "author": {
821
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
822
+ },
823
+ "comments": {
824
+ "_meta": {
825
+ "mode": ["PATCH", "REPLACE"],
826
+ "onRemove": ["DISCONNECT", "DELETE"],
827
+ },
828
+ "author": {
829
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
830
+ },
831
+ },
832
+ "tags": {
833
+ "_meta": {
834
+ "mode": "REPLACE",
835
+ "onRemove": "DELETE",
836
+ },
837
+ },
838
+ },
839
+ "comment": {
840
+ "author": {
841
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
842
+ },
843
+ },
844
+ }
845
+
846
+
847
+ @strawberry.type
848
+ class Mutation:
849
+ create_node = orm.mutations.create_node(project=project)
850
+ update_node = orm.mutations.update_node(project=project)
851
+ ```
852
+
853
+ Rules for the config object:
854
+
855
+ - Root keys are model names (`post`, `comment`, `user`, ...).
856
+ - Nested keys are relation names available on that model.
857
+ - `_meta` is optional and configures behavior for that relation subtree.
858
+ - Omitted relations still exist as shallow nested inputs, but recursion stops after one more level.
859
+
860
+ `_meta` supports:
861
+
862
+ - `mode`: list relation merge strategy (`PATCH` or `REPLACE`)
863
+ - `onRemove`: what to do with removed items from a list relation (`DISCONNECT` or `DELETE`)
864
+ - `onReplace`: what to do with the previous object when replacing a singular relation (`DISCONNECT` or `DELETE`)
865
+
866
+ The `_meta` values can be either:
867
+
868
+ - an array of enum strings, which means the GraphQL input exposes that field and the caller may choose from those options
869
+ - a single enum string, which fixes that behavior for the relation and omits the corresponding GraphQL field
870
+
871
+ Default behavior when a field is exposed but omitted in the mutation input:
872
+
873
+ - `mode` defaults to `PATCH` when allowed
874
+ - `onRemove` defaults to `DISCONNECT` when allowed
875
+ - `onReplace` defaults to `DISCONNECT` when allowed
876
+
877
+ If the preferred default is not included in the allowed array, the first configured value is used.
878
+
879
+ ---
880
+
643
881
  ## Query Optimization
644
882
 
645
883
  Add the optimizer extension to your schema:
@@ -20,9 +20,9 @@ Backend-agnostic schema generation for [Strawberry GraphQL](https://strawberry.r
20
20
  uv add strawberry-orm
21
21
 
22
22
  # With a backend
23
- uv add strawberry-orm[django]
24
- uv add strawberry-orm[sqlalchemy]
25
- uv add strawberry-orm[tortoise]
23
+ uv add "strawberry-orm[django]"
24
+ uv add "strawberry-orm[sqlalchemy]"
25
+ uv add "strawberry-orm[tortoise]"
26
26
  ```
27
27
 
28
28
  You can do the same with `pip`:
@@ -31,6 +31,8 @@ You can do the same with `pip`:
31
31
  pip install "strawberry-orm[sqlalchemy]"
32
32
  ```
33
33
 
34
+ If you are using `zsh`, keep the quotes around extras such as `"strawberry-orm[sqlalchemy]"`. Unquoted square brackets are treated as shell glob syntax before `uv` or `pip` sees the package name.
35
+
34
36
  Requirements:
35
37
 
36
38
  - Python `>=3.12`
@@ -388,6 +390,104 @@ Typical string lookups: `exact`, `neq`, `contains`, `iContains`, `startsWith`, `
388
390
 
389
391
  Regex lookups (`regex`, `iRegex`) are disabled by default. Enable with `enable_regex_filters=True`.
390
392
 
393
+ #### Object Traversal
394
+
395
+ When filters are registered for related models, the generated filter gains an `object` key that lets you filter parent rows based on conditions on their related objects:
396
+
397
+ ```python
398
+ UserFilter = orm.filter(User)
399
+ PostFilter = orm.filter(Post) # Post has an "author" relation to User
400
+ ```
401
+
402
+ ```graphql
403
+ {
404
+ posts(filter: {
405
+ object: { author: { field: { name: { exact: "Alice" } } } }
406
+ }) {
407
+ title
408
+ }
409
+ }
410
+ ```
411
+
412
+ Object traversal composes with boolean operators:
413
+
414
+ ```graphql
415
+ {
416
+ posts(filter: {
417
+ all: [
418
+ { field: { isPublished: { exact: true } } }
419
+ { object: { author: { field: { name: { exact: "Alice" } } } } }
420
+ ]
421
+ }) { title }
422
+ }
423
+ ```
424
+
425
+ Multi-level traversal works when the intermediate models also have registered filters:
426
+
427
+ ```python
428
+ UserFilter = orm.filter(User)
429
+ PostFilter = orm.filter(Post)
430
+ CommentFilter = orm.filter(Comment) # Comment -> Post -> User
431
+ ```
432
+
433
+ ```graphql
434
+ # Find comments on posts written by Alice
435
+ {
436
+ comments(filter: {
437
+ object: { post: {
438
+ object: { author: { field: { name: { exact: "Alice" } } } }
439
+ } }
440
+ }) { body }
441
+ }
442
+ ```
443
+
444
+ The `object` type is `@oneOf`, so each filter entry names exactly one relation.
445
+
446
+ Relations only appear in `object` if their target model already has a registered filter at the time `orm.filter()` is called. Register child model filters before parent model filters.
447
+
448
+ #### Filter Projection
449
+
450
+ By default every relation with a registered filter appears in `object`. Pass `project={...}` to `orm.filter()` to control exactly which relations are exposed and how deep traversal can go:
451
+
452
+ ```python
453
+ UserFilter = orm.filter(User)
454
+ TagFilter = orm.filter(Tag)
455
+ CommentFilter = orm.filter(Comment)
456
+
457
+ # Only expose "author" in the object type — tags and comments are excluded
458
+ PostFilter = orm.filter(Post, project={"author": {}})
459
+ ```
460
+
461
+ ```graphql
462
+ # This works (author is projected)
463
+ { posts(filter: { object: { author: { field: { name: { exact: "Alice" } } } } }) { title } }
464
+
465
+ # This would be a schema error (tags is not projected)
466
+ { posts(filter: { object: { tags: { field: { name: { exact: "python" } } } } }) { title } }
467
+ ```
468
+
469
+ Sub-project dicts control nested traversal. An empty dict `{}` means "include this relation but don't allow further object traversal from it". A non-empty dict lists which of the related model's relations are reachable:
470
+
471
+ ```python
472
+ # Allow Comment -> post, and from post -> author (but not post -> tags)
473
+ CommentFilter = orm.filter(Comment, project={
474
+ "post": {
475
+ "author": {},
476
+ },
477
+ })
478
+ ```
479
+
480
+ Summary:
481
+
482
+ | `project` value | Behavior |
483
+ | --- | --- |
484
+ | `None` (default) | Auto-include all relations with registered filters |
485
+ | `{}` | No `object` type at all (scalar lookups only) |
486
+ | `{"rel": {}}` | Include `rel` as a leaf (no further traversal from it) |
487
+ | `{"rel": {"nested": {}}}` | Include `rel` and allow traversal to `nested` from it |
488
+
489
+ Projected filters are cached internally and do not overwrite the global filter registry, so you can create multiple projected variants of the same model's filter for different schema entry points.
490
+
391
491
  ### Ordering
392
492
 
393
493
  Generate an order input:
@@ -606,6 +706,143 @@ In practice:
606
706
 
607
707
  ---
608
708
 
709
+ ### Recursive Node Mutations
710
+
711
+ `orm.mutations.create_node(...)` and `orm.mutations.update_node(...)` generate catch-all Relay `Node` mutations with recursive nested inputs.
712
+
713
+ If you want to implement the resolver logic yourself, you can generate the root input types directly with `orm.mutations.create_node_input(...)` and `orm.mutations.update_node_input(...)`:
714
+
715
+ ```python
716
+ import strawberry
717
+ from strawberry import relay
718
+
719
+
720
+ @orm.type(User)
721
+ class UserNode(relay.Node):
722
+ id: relay.NodeID[int]
723
+ name: auto
724
+ email: auto
725
+
726
+
727
+ @orm.type(Post)
728
+ class PostNode(relay.Node):
729
+ id: relay.NodeID[int]
730
+ title: auto
731
+ body: auto
732
+
733
+
734
+ CreateNodeInput = orm.mutations.create_node_input()
735
+ UpdateNodeInput = orm.mutations.update_node_input()
736
+
737
+
738
+ @strawberry.type
739
+ class Mutation:
740
+ create_node = orm.mutations.create_node()
741
+ update_node = orm.mutations.update_node()
742
+
743
+ @strawberry.field
744
+ def custom_create_node(self, input: CreateNodeInput) -> str:
745
+ return "implement your own create logic here"
746
+
747
+ @strawberry.field
748
+ def custom_update_node(self, input: UpdateNodeInput) -> str:
749
+ return "implement your own update logic here"
750
+ ```
751
+
752
+ List relations use `items`, while singular relations use explicit `create` / `update` branches:
753
+
754
+ ```graphql
755
+ mutation {
756
+ createNode(input: {
757
+ post: {
758
+ title: "Hello"
759
+ body: "World"
760
+ author: {
761
+ create: {
762
+ name: "Alice"
763
+ email: "alice@example.com"
764
+ }
765
+ }
766
+ tags: {
767
+ items: [{ create: { name: "python" } }]
768
+ mode: REPLACE
769
+ onRemove: DELETE
770
+ }
771
+ }
772
+ }) {
773
+ __typename
774
+ }
775
+ }
776
+ ```
777
+
778
+ #### Projection And Policy Config
779
+
780
+ Pass `project={...}` to restrict recursion depth and configure relation semantics in one dict:
781
+
782
+ ```python
783
+ project = {
784
+ "post": {
785
+ "author": {
786
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
787
+ },
788
+ "comments": {
789
+ "_meta": {
790
+ "mode": ["PATCH", "REPLACE"],
791
+ "onRemove": ["DISCONNECT", "DELETE"],
792
+ },
793
+ "author": {
794
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
795
+ },
796
+ },
797
+ "tags": {
798
+ "_meta": {
799
+ "mode": "REPLACE",
800
+ "onRemove": "DELETE",
801
+ },
802
+ },
803
+ },
804
+ "comment": {
805
+ "author": {
806
+ "_meta": {"onReplace": ["DISCONNECT", "DELETE"]},
807
+ },
808
+ },
809
+ }
810
+
811
+
812
+ @strawberry.type
813
+ class Mutation:
814
+ create_node = orm.mutations.create_node(project=project)
815
+ update_node = orm.mutations.update_node(project=project)
816
+ ```
817
+
818
+ Rules for the config object:
819
+
820
+ - Root keys are model names (`post`, `comment`, `user`, ...).
821
+ - Nested keys are relation names available on that model.
822
+ - `_meta` is optional and configures behavior for that relation subtree.
823
+ - Omitted relations still exist as shallow nested inputs, but recursion stops after one more level.
824
+
825
+ `_meta` supports:
826
+
827
+ - `mode`: list relation merge strategy (`PATCH` or `REPLACE`)
828
+ - `onRemove`: what to do with removed items from a list relation (`DISCONNECT` or `DELETE`)
829
+ - `onReplace`: what to do with the previous object when replacing a singular relation (`DISCONNECT` or `DELETE`)
830
+
831
+ The `_meta` values can be either:
832
+
833
+ - an array of enum strings, which means the GraphQL input exposes that field and the caller may choose from those options
834
+ - a single enum string, which fixes that behavior for the relation and omits the corresponding GraphQL field
835
+
836
+ Default behavior when a field is exposed but omitted in the mutation input:
837
+
838
+ - `mode` defaults to `PATCH` when allowed
839
+ - `onRemove` defaults to `DISCONNECT` when allowed
840
+ - `onReplace` defaults to `DISCONNECT` when allowed
841
+
842
+ If the preferred default is not included in the allowed array, the first configured value is used.
843
+
844
+ ---
845
+
609
846
  ## Query Optimization
610
847
 
611
848
  Add the optimizer extension to your schema:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "strawberry-orm"
3
- version = "0.2.0"
3
+ version = "0.4.0"
4
4
  description = "Unified, backend-agnostic ORM abstraction for Strawberry GraphQL"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -33,6 +33,7 @@ django = ["django>=4.2"]
33
33
  sqlalchemy = ["sqlalchemy>=2.0"]
34
34
  tortoise = ["tortoise-orm>=0.25.0"]
35
35
  dev = [
36
+ "greenlet>=3.3.2",
36
37
  "pytest>=8.0",
37
38
  "pytest-asyncio>=0.24",
38
39
  "pytest-django>=4.8",
@@ -46,6 +47,7 @@ build-backend = "uv_build"
46
47
 
47
48
  [dependency-groups]
48
49
  dev = [
50
+ "pytest-cov>=7.0.0",
49
51
  "starlette>=0.52.1",
50
52
  "typer>=0.24.1",
51
53
  "uvicorn>=0.41.0",