karrio-server-graph 2025.5__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.
Files changed (39) hide show
  1. karrio/server/graph/__init__.py +1 -0
  2. karrio/server/graph/admin.py +3 -0
  3. karrio/server/graph/apps.py +5 -0
  4. karrio/server/graph/forms.py +57 -0
  5. karrio/server/graph/management/__init__.py +0 -0
  6. karrio/server/graph/management/commands/__init__.py +0 -0
  7. karrio/server/graph/management/commands/export_schema.py +9 -0
  8. karrio/server/graph/migrations/0001_initial.py +37 -0
  9. karrio/server/graph/migrations/0002_auto_20210512_1353.py +22 -0
  10. karrio/server/graph/migrations/__init__.py +0 -0
  11. karrio/server/graph/models.py +44 -0
  12. karrio/server/graph/schema.py +44 -0
  13. karrio/server/graph/schemas/__init__.py +2 -0
  14. karrio/server/graph/schemas/base/__init__.py +385 -0
  15. karrio/server/graph/schemas/base/inputs.py +612 -0
  16. karrio/server/graph/schemas/base/mutations.py +1033 -0
  17. karrio/server/graph/schemas/base/types.py +1406 -0
  18. karrio/server/graph/serializers.py +388 -0
  19. karrio/server/graph/templates/graphql/graphiql.html +142 -0
  20. karrio/server/graph/templates/karrio/email_change_email.html +13 -0
  21. karrio/server/graph/templates/karrio/email_change_email.txt +13 -0
  22. karrio/server/graph/templates/karrio/password_reset_email.html +14 -0
  23. karrio/server/graph/tests/__init__.py +9 -0
  24. karrio/server/graph/tests/base.py +153 -0
  25. karrio/server/graph/tests/test_carrier_connections.py +239 -0
  26. karrio/server/graph/tests/test_metafield.py +404 -0
  27. karrio/server/graph/tests/test_partial_shipments.py +603 -0
  28. karrio/server/graph/tests/test_rate_sheets.py +354 -0
  29. karrio/server/graph/tests/test_registration.py +209 -0
  30. karrio/server/graph/tests/test_templates.py +677 -0
  31. karrio/server/graph/tests/test_user_info.py +71 -0
  32. karrio/server/graph/urls.py +10 -0
  33. karrio/server/graph/utils.py +308 -0
  34. karrio/server/graph/views.py +91 -0
  35. karrio/server/settings/graph.py +7 -0
  36. karrio_server_graph-2025.5.dist-info/METADATA +29 -0
  37. karrio_server_graph-2025.5.dist-info/RECORD +39 -0
  38. karrio_server_graph-2025.5.dist-info/WHEEL +5 -0
  39. karrio_server_graph-2025.5.dist-info/top_level.txt +2 -0
@@ -0,0 +1,388 @@
1
+ import typing
2
+ import strawberry
3
+ from django.db import transaction
4
+ from django.contrib.auth import get_user_model
5
+ from django.utils.translation import gettext_lazy as _
6
+ from rest_framework import exceptions
7
+
8
+ import karrio.server.serializers as serializers
9
+ import karrio.server.core.validators as validators
10
+ import karrio.server.providers.models as providers
11
+ import karrio.server.manager.models as manager
12
+ import karrio.server.graph.models as graph
13
+ import karrio.server.core.models as core
14
+ import karrio.server.user.models as auth
15
+ import karrio.server.core.gateway as gateway
16
+
17
+
18
+ class UserModelSerializer(serializers.ModelSerializer):
19
+ email = serializers.CharField(required=False)
20
+
21
+ class Meta:
22
+ model = get_user_model()
23
+ extra_kwargs = {
24
+ field: {"read_only": True}
25
+ for field in ["id", "is_staff", "last_login", "date_joined"]
26
+ }
27
+ fields = [
28
+ "email",
29
+ "full_name",
30
+ "is_active",
31
+ "is_staff",
32
+ "last_login",
33
+ "date_joined",
34
+ "metadata",
35
+ ]
36
+
37
+ @transaction.atomic
38
+ def update(self, instance, data: dict, **kwargs):
39
+ user = super().update(instance, data)
40
+
41
+ if data.get("is_active") == False:
42
+ user.save(update_fields=["is_active"])
43
+
44
+ return user
45
+
46
+
47
+ @serializers.owned_model_serializer
48
+ class WorkspaceConfigModelSerializer(serializers.ModelSerializer):
49
+ class Meta:
50
+ model = auth.WorkspaceConfig
51
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
52
+ exclude = ["created_at", "updated_at", "created_by"]
53
+
54
+ def create(
55
+ self, validated_data: dict, context: serializers.Context = None, **kwargs
56
+ ):
57
+ instance = super().create(validated_data, context=context, **kwargs)
58
+
59
+ if (
60
+ hasattr(auth.WorkspaceConfig, "org")
61
+ and getattr(context, "org", None) is not None
62
+ ):
63
+ context.org.config = instance
64
+ context.org.save()
65
+
66
+ return instance
67
+
68
+
69
+ @serializers.owned_model_serializer
70
+ class MetafieldModelSerializer(serializers.ModelSerializer):
71
+ class Meta:
72
+ model = core.Metafield
73
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
74
+ exclude = ["created_at", "updated_at", "created_by"]
75
+
76
+
77
+ @serializers.owned_model_serializer
78
+ class AddressModelSerializer(
79
+ validators.AugmentedAddressSerializer, serializers.ModelSerializer
80
+ ):
81
+ country_code = serializers.CharField(required=False)
82
+
83
+ class Meta:
84
+ model = manager.Address
85
+ extra_kwargs = {field: {"read_only": True} for field in ["id", "validation"]}
86
+ exclude = ["created_at", "updated_at", "created_by", "validation"]
87
+
88
+
89
+ @serializers.owned_model_serializer
90
+ class CommodityModelSerializer(serializers.ModelSerializer):
91
+ weight_unit = serializers.CharField()
92
+ value_currency = serializers.CharField(required=False)
93
+ origin_country = serializers.CharField(required=False)
94
+
95
+ class Meta:
96
+ model = manager.Commodity
97
+ exclude = ["created_at", "updated_at", "created_by", "parent"]
98
+ extra_kwargs = {field: {"read_only": True} for field in ["id", "parent"]}
99
+
100
+
101
+ @serializers.owned_model_serializer
102
+ class CustomsModelSerializer(serializers.ModelSerializer):
103
+ NESTED_FIELDS = ["commodities"]
104
+
105
+ incoterm = serializers.CharField(required=False, allow_null=True, allow_blank=True)
106
+ commodities = serializers.make_fields_optional(CommodityModelSerializer)(
107
+ many=True, allow_null=True, required=False
108
+ )
109
+
110
+ class Meta:
111
+ model = manager.Customs
112
+ exclude = ["created_at", "updated_at", "created_by"]
113
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
114
+
115
+ @transaction.atomic
116
+ def create(self, validated_data: dict, context: dict):
117
+ data = {
118
+ name: value
119
+ for name, value in validated_data.items()
120
+ if name not in self.NESTED_FIELDS
121
+ }
122
+
123
+ instance = super().create(data)
124
+
125
+ serializers.save_many_to_many_data(
126
+ "commodities",
127
+ CommodityModelSerializer,
128
+ instance,
129
+ payload=validated_data,
130
+ context=context,
131
+ )
132
+
133
+ return instance
134
+
135
+ @transaction.atomic
136
+ def update(
137
+ self, instance: manager.Customs, validated_data: dict, **kwargs
138
+ ) -> manager.Customs:
139
+ data = {
140
+ name: value
141
+ for name, value in validated_data.items()
142
+ if name not in self.NESTED_FIELDS
143
+ }
144
+
145
+ return super().update(instance, data)
146
+
147
+
148
+ @serializers.owned_model_serializer
149
+ class ParcelModelSerializer(validators.PresetSerializer, serializers.ModelSerializer):
150
+ weight_unit = serializers.CharField(
151
+ required=False, allow_null=True, allow_blank=True
152
+ )
153
+ dimension_unit = serializers.CharField(
154
+ required=False, allow_null=True, allow_blank=True
155
+ )
156
+
157
+ class Meta:
158
+ model = manager.Parcel
159
+ exclude = ["created_at", "updated_at", "created_by", "items"]
160
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
161
+
162
+
163
+ @serializers.owned_model_serializer
164
+ class TemplateModelSerializer(serializers.ModelSerializer):
165
+ address = serializers.make_fields_optional(AddressModelSerializer)(required=False)
166
+ customs = serializers.make_fields_optional(CustomsModelSerializer)(required=False)
167
+ parcel = serializers.make_fields_optional(ParcelModelSerializer)(required=False)
168
+
169
+ class Meta:
170
+ model = graph.Template
171
+ exclude = ["created_at", "updated_at", "created_by"]
172
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
173
+
174
+ @transaction.atomic
175
+ def create(self, validated_data: dict, context: dict, **kwargs) -> graph.Template:
176
+ data = {
177
+ **validated_data,
178
+ "address": serializers.save_one_to_one_data(
179
+ "address",
180
+ AddressModelSerializer,
181
+ payload=validated_data,
182
+ context=context,
183
+ ),
184
+ "customs": serializers.save_one_to_one_data(
185
+ "customs",
186
+ CustomsModelSerializer,
187
+ payload=validated_data,
188
+ context=context,
189
+ ),
190
+ "parcel": serializers.save_one_to_one_data(
191
+ "parcel", ParcelModelSerializer, payload=validated_data, context=context
192
+ ),
193
+ }
194
+
195
+ ensure_unique_default_related_data(validated_data, context=context)
196
+
197
+ return super().create(data)
198
+
199
+ @transaction.atomic
200
+ def update(
201
+ self, instance: graph.Template, validated_data: dict, **kwargs
202
+ ) -> graph.Template:
203
+ data = {
204
+ key: value
205
+ for key, value in validated_data.items()
206
+ if key not in ["address", "customs", "parcel"]
207
+ }
208
+
209
+ serializers.save_one_to_one_data(
210
+ "address", AddressModelSerializer, instance, payload=validated_data
211
+ )
212
+ serializers.save_one_to_one_data(
213
+ "customs", CustomsModelSerializer, instance, payload=validated_data
214
+ )
215
+ serializers.save_one_to_one_data(
216
+ "parcel", ParcelModelSerializer, instance, payload=validated_data
217
+ )
218
+
219
+ ensure_unique_default_related_data(validated_data, instance)
220
+
221
+ return super().update(instance, data)
222
+
223
+
224
+ def ensure_unique_default_related_data(
225
+ data: dict = None, instance: typing.Optional[graph.Template] = None, context=None
226
+ ):
227
+ _get = lambda key: data.get(key, getattr(instance, key, None))
228
+ if _get("is_default") is not True:
229
+ return
230
+
231
+ if _get("address") is not None:
232
+ query = dict(address__isnull=False, is_default=True)
233
+ elif _get("customs") is not None:
234
+ query = dict(customs__isnull=False, is_default=True)
235
+ elif _get("parcel") is not None:
236
+ query = dict(parcel__isnull=False, is_default=True)
237
+ else:
238
+ return
239
+
240
+ graph.Template.access_by(context or instance.created_by).exclude(
241
+ id=_get("id")
242
+ ).filter(**query).update(is_default=False)
243
+
244
+
245
+ @serializers.owned_model_serializer
246
+ class ServiceLevelModelSerializer(serializers.ModelSerializer):
247
+ dimension_unit = serializers.CharField(
248
+ required=False, allow_null=True, allow_blank=True
249
+ )
250
+ weight_unit = serializers.CharField(
251
+ required=False, allow_null=True, allow_blank=True
252
+ )
253
+ currency = serializers.CharField(required=False, allow_null=True, allow_blank=True)
254
+
255
+ class Meta:
256
+ model = providers.ServiceLevel
257
+ exclude = ["created_at", "updated_at", "created_by"]
258
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
259
+
260
+ def update_zone(self, zone_index: int, zone_data: dict) -> None:
261
+ """Update a specific zone in the service level."""
262
+ if zone_index >= len(self.instance.zones):
263
+ raise exceptions.ValidationError(
264
+ _(f"Zone index {zone_index} is out of range"),
265
+ code="invalid_zone_index",
266
+ )
267
+
268
+ self.instance.zones[zone_index].update(
269
+ {k: v for k, v in zone_data.items() if v != strawberry.UNSET}
270
+ )
271
+ self.instance.save(update_fields=["zones"])
272
+
273
+ def update(self, instance, validated_data, context=None, **kwargs):
274
+ """Handle partial updates of service level data including zones."""
275
+ zones_data = validated_data.pop("zones", None)
276
+ instance = super().update(instance, validated_data, context=context)
277
+
278
+ if zones_data is not None:
279
+ # Handle zone updates if provided
280
+ existing_zones = instance.zones or []
281
+ updated_zones = []
282
+
283
+ for idx, zone_data in enumerate(zones_data):
284
+ if idx < len(existing_zones):
285
+ # Update existing zone
286
+ zone = existing_zones[idx].copy()
287
+ zone.update(
288
+ {k: v for k, v in zone_data.items() if v != strawberry.UNSET}
289
+ )
290
+ updated_zones.append(zone)
291
+ else:
292
+ # Add new zone
293
+ updated_zones.append(zone_data)
294
+
295
+ instance.zones = updated_zones
296
+ instance.save(update_fields=["zones"])
297
+
298
+ return instance
299
+
300
+
301
+ @serializers.owned_model_serializer
302
+ class LabelTemplateModelSerializer(serializers.ModelSerializer):
303
+ template_type = serializers.CharField(required=False)
304
+
305
+ class Meta:
306
+ model = providers.LabelTemplate
307
+ exclude = ["created_at", "updated_at", "created_by"]
308
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
309
+
310
+
311
+ @serializers.owned_model_serializer
312
+ class RateSheetModelSerializer(serializers.ModelSerializer):
313
+ class Meta:
314
+ model = providers.RateSheet
315
+ exclude = ["created_at", "updated_at", "created_by"]
316
+ extra_kwargs = {field: {"read_only": True} for field in ["id", "services"]}
317
+
318
+ def update_services(
319
+ self, services_data: list, remove_missing: bool = False
320
+ ) -> None:
321
+ """Update services of the rate sheet."""
322
+ existing_services = {s.id: s for s in self.instance.services.all()}
323
+
324
+ for service_data in services_data:
325
+ service_id = service_data.get("id")
326
+ if service_id and service_id in existing_services:
327
+ # Update existing service
328
+ service = existing_services[service_id]
329
+ service_serializer = ServiceLevelModelSerializer(
330
+ service,
331
+ data=service_data,
332
+ context=self.context,
333
+ partial=True,
334
+ )
335
+ service_serializer.is_valid(raise_exception=True)
336
+ service_serializer.save()
337
+ else:
338
+ # Create new service
339
+ service_serializer = ServiceLevelModelSerializer(
340
+ data=service_data,
341
+ context=self.context,
342
+ )
343
+ service_serializer.is_valid(raise_exception=True)
344
+ service = service_serializer.save()
345
+ self.instance.services.add(service)
346
+
347
+ # Remove services that are not in the update
348
+ if remove_missing:
349
+ service_ids = {s.get("id") for s in services_data if "id" in s}
350
+ for service in existing_services.values():
351
+ if service.id not in service_ids:
352
+ self.instance.services.remove(service)
353
+ service.delete()
354
+
355
+ def update_carriers(self, carriers: list) -> None:
356
+ """Update carrier associations."""
357
+ if carriers is not None:
358
+ _ids = set(
359
+ [*carriers, *(self.instance.carriers.values_list("id", flat=True))]
360
+ )
361
+ _carriers = gateway.Carriers.list(
362
+ context=self.context,
363
+ carrier_name=self.instance.carrier_name,
364
+ ).filter(id__in=list(_ids))
365
+
366
+ for carrier in _carriers:
367
+ carrier.rate_sheet = self.instance if carrier.id in carriers else None
368
+ carrier.save(update_fields=["rate_sheet"])
369
+
370
+ def update(self, instance, validated_data, **kwargs):
371
+ """Handle updates of rate sheet data including services and carriers."""
372
+ services_data = validated_data.pop("services", None)
373
+ carriers = (
374
+ validated_data.pop("carriers", None)
375
+ if "carriers" in validated_data
376
+ else None
377
+ )
378
+ remove_missing_services = validated_data.pop("remove_missing_services", False)
379
+
380
+ instance = super().update(instance, validated_data)
381
+
382
+ if services_data is not None:
383
+ self.update_services(services_data, remove_missing_services)
384
+
385
+ if carriers is not None:
386
+ self.update_carriers(carriers)
387
+
388
+ return instance
@@ -0,0 +1,142 @@
1
+ {% load i18n static %}
2
+ <!DOCTYPE html>
3
+ <html>
4
+
5
+ <head>
6
+ <meta charset="utf-8">
7
+ <meta name="theme-color" content="#ffffff">
8
+ <link rel="shortcut icon" href="{% static 'branding/favicon.ico' %}">
9
+
10
+ <title>{{ APP_NAME }} Graph</title>
11
+
12
+ <style>
13
+ html, body {
14
+ height: 100%;
15
+ margin: 0;
16
+ overflow: hidden;
17
+ width: 100%;
18
+ }
19
+
20
+ #graphiql {
21
+ height: 100vh;
22
+ display: flex;
23
+ }
24
+
25
+ .docExplorerHide {
26
+ display: none;
27
+ }
28
+
29
+ .doc-explorer-contents {
30
+ overflow-y: hidden !important;
31
+ }
32
+
33
+ .docExplorerWrap {
34
+ width: unset !important;
35
+ min-width: unset !important;
36
+ }
37
+
38
+ .graphiql-explorer-actions select {
39
+ margin-left: 4px;
40
+ }
41
+
42
+ .admin-link {
43
+ position: fixed;
44
+ bottom: 44px;
45
+ right: 30px;
46
+ width: 40px;
47
+ height: 40px;
48
+ background-color: rgba(50, 50, 159, 70%);
49
+ border-radius: 50%;
50
+ z-index: 10;
51
+ box-shadow: rgb(0 0 0 / 30%) 0px 0px 20px;
52
+ }
53
+
54
+ @media screen and (max-width: 50rem) {
55
+ .admin-link {
56
+ bottom: 112px;
57
+ }
58
+ }
59
+ </style>
60
+
61
+ <script
62
+ crossorigin
63
+ src="https://unpkg.com/react@17.0.2/umd/react.development.js"
64
+ integrity="sha384-xQwCoNcK/7P3Lpv50IZSEbJdpqbToWEODAUyI/RECaRXmOE2apWt7htari8kvKa/"
65
+ ></script>
66
+ <script
67
+ crossorigin
68
+ src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"
69
+ integrity="sha384-E9IgxDsnjKgh0777N3lXen7NwXeTsOpLLJhI01SW7idG046SRqJpsW2rJwsOYk0L"
70
+ ></script>
71
+ <script
72
+ crossorigin
73
+ src="https://unpkg.com/js-cookie@3.0.1/dist/js.cookie.min.js"
74
+ integrity="sha384-ETDm/j6COkRSUfVFsGNM5WYE4WjyRgfDhy4Pf4Fsc8eNw/eYEMqYZWuxTzMX6FBa"
75
+ ></script>
76
+
77
+ <link
78
+ crossorigin
79
+ rel="stylesheet"
80
+ href="https://unpkg.com/graphiql@2.0.3/graphiql.min.css"
81
+ integrity="sha384-AKx2Bh1kuZ1tUTwbmHASXvBtHBX4WWVwdTQjArDlqPCL2uuBTyJkajuxdczWhzTN"
82
+ />
83
+ </head>
84
+
85
+ <body>
86
+ <div id="graphiql" class="graphiql-container">Loading...</div>
87
+
88
+ <script
89
+ crossorigin
90
+ src="https://unpkg.com/graphiql@2.0.3/graphiql.min.js"
91
+ integrity="sha384-WI6ayyBMb7Ln13us9JlWopMH4Kz33Pt9bYbkO5oY/xryP/pbGmz5Q08oS2dcrLmc"
92
+ ></script>
93
+ <script
94
+ crossorigin
95
+ src="https://unpkg.com/@graphiql/plugin-explorer@0.1.0/dist/graphiql-plugin-explorer.umd.js"
96
+ integrity="sha384-XyAmNqmxnLsRHkMhQYTqC0ub7uXpNbwdkhjn70ZF3J3XSb7bouSdRVfzDojimcMd"
97
+ ></script>
98
+ <script>
99
+ const EXAMPLE_QUERY = `# Welcome to GraphiQL`;
100
+
101
+ const fetchURL = window.location.href;
102
+
103
+ function httpUrlToWebSockeUrl(url) {
104
+ return url.replace(/(http)(s)?\:\/\//, "ws$2://");
105
+ }
106
+
107
+ const headers = {};
108
+ const csrfToken = Cookies.get("csrftoken");
109
+
110
+ if (csrfToken) {
111
+ headers["x-csrftoken"] = csrfToken;
112
+ }
113
+
114
+ const fetcher = GraphiQL.createFetcher({
115
+ url: fetchURL,
116
+ headers: headers,
117
+ });
118
+
119
+ function GraphiQLWithExplorer() {
120
+ const [query, setQuery] = React.useState(EXAMPLE_QUERY);
121
+ const explorerPlugin = GraphiQLPluginExplorer.useExplorerPlugin({
122
+ query: query,
123
+ onEdit: setQuery,
124
+ });
125
+ return React.createElement(GraphiQL, {
126
+ fetcher: fetcher,
127
+ defaultEditorToolsVisibility: true,
128
+ plugins: [explorerPlugin],
129
+ query: query,
130
+ onEditQuery: setQuery,
131
+ graphiqlHeaderEditorEnabled: true,
132
+ });
133
+ }
134
+
135
+ ReactDOM.render(
136
+ React.createElement(GraphiQLWithExplorer),
137
+ document.getElementById("graphiql")
138
+ );
139
+ </script>
140
+ </body>
141
+
142
+ </html>
@@ -0,0 +1,13 @@
1
+ {% load i18n %}{% autoescape off %}
2
+ {% blocktrans %}You're receiving this email because you requested an email change for your user account at {{ APP_NAME }}.{% endblocktrans %}
3
+ <br /><br />
4
+ {% trans "Please open the following link to confirm:" %}<br />
5
+ {% block change_link %}
6
+ <a href="{{ link }}?token={{ token }}" target="_blank" rel="noopener noreferrer">{{ link }}?token={{ token }}</a>
7
+ {% endblock %}
8
+ <br /><br />
9
+ {% trans "Thanks for using our platform!" %}
10
+ <br />
11
+ {% blocktrans %}The {{ APP_NAME }} team{% endblocktrans %}
12
+
13
+ {% endautoescape %}
@@ -0,0 +1,13 @@
1
+ {% load i18n %}{% autoescape off %}
2
+ {% blocktrans %}You're receiving this email because you requested an email change for your user account at {{ APP_NAME }}.{% endblocktrans %}
3
+
4
+ {% trans "Please open the following link to confirm:" %}
5
+ {% block change_link %}
6
+ {{ link }}?token={{ token }}
7
+ {% endblock %}
8
+
9
+ {% trans "Thanks for using our platform!" %}
10
+
11
+ {% blocktrans %}The {{ APP_NAME }} team{% endblocktrans %}
12
+
13
+ {% endautoescape %}
@@ -0,0 +1,14 @@
1
+ {% load i18n %}{% autoescape off %}
2
+ {% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ app_name }}.{% endblocktrans %}
3
+
4
+ {% trans "Please go to the following page and choose a new password:" %}
5
+ {% block reset_link %}
6
+ {{ redirect_url }}?uidb64={{ uid }}&token={{ token }}
7
+ {% endblock %}
8
+ {% trans 'Your username, in case you’ve forgotten:' %} {{ user.get_username }}
9
+
10
+ {% trans "Thanks for using our platform!" %}
11
+
12
+ {% blocktrans %}The {{ app_name }} team{% endblocktrans %}
13
+
14
+ {% endautoescape %}
@@ -0,0 +1,9 @@
1
+ import logging
2
+
3
+ logging.disable(logging.CRITICAL)
4
+
5
+ from karrio.server.graph.tests.test_templates import *
6
+ from karrio.server.graph.tests.test_carrier_connections import *
7
+ from karrio.server.graph.tests.test_user_info import *
8
+ from karrio.server.graph.tests.test_rate_sheets import *
9
+ from karrio.server.graph.tests.test_metafield import *