karrio-server-graph 2025.5rc1__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 (37) 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 +59 -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 +46 -0
  13. karrio/server/graph/schemas/__init__.py +2 -0
  14. karrio/server/graph/schemas/base/__init__.py +367 -0
  15. karrio/server/graph/schemas/base/inputs.py +582 -0
  16. karrio/server/graph/schemas/base/mutations.py +871 -0
  17. karrio/server/graph/schemas/base/types.py +1365 -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 +124 -0
  25. karrio/server/graph/tests/test_carrier_connections.py +219 -0
  26. karrio/server/graph/tests/test_metafield.py +404 -0
  27. karrio/server/graph/tests/test_rate_sheets.py +348 -0
  28. karrio/server/graph/tests/test_templates.py +677 -0
  29. karrio/server/graph/tests/test_user_info.py +71 -0
  30. karrio/server/graph/urls.py +10 -0
  31. karrio/server/graph/utils.py +304 -0
  32. karrio/server/graph/views.py +93 -0
  33. karrio/server/settings/graph.py +7 -0
  34. karrio_server_graph-2025.5rc1.dist-info/METADATA +29 -0
  35. karrio_server_graph-2025.5rc1.dist-info/RECORD +37 -0
  36. karrio_server_graph-2025.5rc1.dist-info/WHEEL +5 -0
  37. karrio_server_graph-2025.5rc1.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
+
16
+
17
+ class UserModelSerializer(serializers.ModelSerializer):
18
+ email = serializers.CharField(required=False)
19
+
20
+ class Meta:
21
+ model = get_user_model()
22
+ extra_kwargs = {
23
+ field: {"read_only": True}
24
+ for field in ["id", "is_staff", "last_login", "date_joined"]
25
+ }
26
+ fields = [
27
+ "email",
28
+ "full_name",
29
+ "is_active",
30
+ "is_staff",
31
+ "last_login",
32
+ "date_joined",
33
+ ]
34
+
35
+ @transaction.atomic
36
+ def update(self, instance, data: dict, **kwargs):
37
+ user = super().update(instance, data)
38
+
39
+ if data.get("is_active") == False:
40
+ user.save(update_fields=["is_active"])
41
+
42
+ return user
43
+
44
+
45
+ @serializers.owned_model_serializer
46
+ class WorkspaceConfigModelSerializer(serializers.ModelSerializer):
47
+ class Meta:
48
+ model = auth.WorkspaceConfig
49
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
50
+ exclude = ["created_at", "updated_at", "created_by"]
51
+
52
+ def create(
53
+ self, validated_data: dict, context: serializers.Context = None, **kwargs
54
+ ):
55
+ instance = super().create(validated_data, context=context, **kwargs)
56
+
57
+ if (
58
+ hasattr(auth.WorkspaceConfig, "org")
59
+ and getattr(context, "org", None) is not None
60
+ ):
61
+ context.org.config = instance
62
+ context.org.save()
63
+
64
+ return instance
65
+
66
+
67
+ @serializers.owned_model_serializer
68
+ class MetafieldModelSerializer(serializers.ModelSerializer):
69
+ class Meta:
70
+ model = core.Metafield
71
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
72
+ exclude = ["created_at", "updated_at", "created_by"]
73
+
74
+
75
+ @serializers.owned_model_serializer
76
+ class AddressModelSerializer(
77
+ validators.AugmentedAddressSerializer, serializers.ModelSerializer
78
+ ):
79
+ country_code = serializers.CharField(required=False)
80
+
81
+ class Meta:
82
+ model = manager.Address
83
+ extra_kwargs = {field: {"read_only": True} for field in ["id", "validation"]}
84
+ exclude = ["created_at", "updated_at", "created_by", "validation"]
85
+
86
+
87
+ @serializers.owned_model_serializer
88
+ class CommodityModelSerializer(serializers.ModelSerializer):
89
+ weight_unit = serializers.CharField()
90
+ value_currency = serializers.CharField(required=False)
91
+ origin_country = serializers.CharField(required=False)
92
+
93
+ class Meta:
94
+ model = manager.Commodity
95
+ exclude = ["created_at", "updated_at", "created_by", "parent"]
96
+ extra_kwargs = {field: {"read_only": True} for field in ["id", "parent"]}
97
+
98
+
99
+ @serializers.owned_model_serializer
100
+ class CustomsModelSerializer(serializers.ModelSerializer):
101
+ NESTED_FIELDS = ["commodities"]
102
+
103
+ incoterm = serializers.CharField(required=False, allow_null=True, allow_blank=True)
104
+ commodities = serializers.make_fields_optional(CommodityModelSerializer)(
105
+ many=True, allow_null=True, required=False
106
+ )
107
+
108
+ class Meta:
109
+ model = manager.Customs
110
+ exclude = ["created_at", "updated_at", "created_by"]
111
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
112
+
113
+ @transaction.atomic
114
+ def create(self, validated_data: dict, context: dict):
115
+ data = {
116
+ name: value
117
+ for name, value in validated_data.items()
118
+ if name not in self.NESTED_FIELDS
119
+ }
120
+
121
+ instance = super().create(data)
122
+
123
+ serializers.save_many_to_many_data(
124
+ "commodities",
125
+ CommodityModelSerializer,
126
+ instance,
127
+ payload=validated_data,
128
+ context=context,
129
+ )
130
+
131
+ return instance
132
+
133
+ @transaction.atomic
134
+ def update(
135
+ self, instance: manager.Customs, validated_data: dict, **kwargs
136
+ ) -> manager.Customs:
137
+ data = {
138
+ name: value
139
+ for name, value in validated_data.items()
140
+ if name not in self.NESTED_FIELDS
141
+ }
142
+
143
+ return super().update(instance, data)
144
+
145
+
146
+ @serializers.owned_model_serializer
147
+ class ParcelModelSerializer(validators.PresetSerializer, serializers.ModelSerializer):
148
+ weight_unit = serializers.CharField(
149
+ required=False, allow_null=True, allow_blank=True
150
+ )
151
+ dimension_unit = serializers.CharField(
152
+ required=False, allow_null=True, allow_blank=True
153
+ )
154
+
155
+ class Meta:
156
+ model = manager.Parcel
157
+ exclude = ["created_at", "updated_at", "created_by", "items"]
158
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
159
+
160
+
161
+ @serializers.owned_model_serializer
162
+ class TemplateModelSerializer(serializers.ModelSerializer):
163
+ address = serializers.make_fields_optional(AddressModelSerializer)(required=False)
164
+ customs = serializers.make_fields_optional(CustomsModelSerializer)(required=False)
165
+ parcel = serializers.make_fields_optional(ParcelModelSerializer)(required=False)
166
+
167
+ class Meta:
168
+ model = graph.Template
169
+ exclude = ["created_at", "updated_at", "created_by"]
170
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
171
+
172
+ @transaction.atomic
173
+ def create(self, validated_data: dict, context: dict, **kwargs) -> graph.Template:
174
+ data = {
175
+ **validated_data,
176
+ "address": serializers.save_one_to_one_data(
177
+ "address",
178
+ AddressModelSerializer,
179
+ payload=validated_data,
180
+ context=context,
181
+ ),
182
+ "customs": serializers.save_one_to_one_data(
183
+ "customs",
184
+ CustomsModelSerializer,
185
+ payload=validated_data,
186
+ context=context,
187
+ ),
188
+ "parcel": serializers.save_one_to_one_data(
189
+ "parcel", ParcelModelSerializer, payload=validated_data, context=context
190
+ ),
191
+ }
192
+
193
+ ensure_unique_default_related_data(validated_data, context=context)
194
+
195
+ return super().create(data)
196
+
197
+ @transaction.atomic
198
+ def update(
199
+ self, instance: graph.Template, validated_data: dict, **kwargs
200
+ ) -> graph.Template:
201
+ data = {
202
+ key: value
203
+ for key, value in validated_data.items()
204
+ if key not in ["address", "customs", "parcel"]
205
+ }
206
+
207
+ serializers.save_one_to_one_data(
208
+ "address", AddressModelSerializer, instance, payload=validated_data
209
+ )
210
+ serializers.save_one_to_one_data(
211
+ "customs", CustomsModelSerializer, instance, payload=validated_data
212
+ )
213
+ serializers.save_one_to_one_data(
214
+ "parcel", ParcelModelSerializer, instance, payload=validated_data
215
+ )
216
+
217
+ ensure_unique_default_related_data(validated_data, instance)
218
+
219
+ return super().update(instance, data)
220
+
221
+
222
+ def ensure_unique_default_related_data(
223
+ data: dict = None, instance: typing.Optional[graph.Template] = None, context=None
224
+ ):
225
+ _get = lambda key: data.get(key, getattr(instance, key, None))
226
+ if _get("is_default") is not True:
227
+ return
228
+
229
+ if _get("address") is not None:
230
+ query = dict(address__isnull=False, is_default=True)
231
+ elif _get("customs") is not None:
232
+ query = dict(customs__isnull=False, is_default=True)
233
+ elif _get("parcel") is not None:
234
+ query = dict(parcel__isnull=False, is_default=True)
235
+ else:
236
+ return
237
+
238
+ graph.Template.access_by(context or instance.created_by).exclude(
239
+ id=_get("id")
240
+ ).filter(**query).update(is_default=False)
241
+
242
+
243
+ @serializers.owned_model_serializer
244
+ class ServiceLevelModelSerializer(serializers.ModelSerializer):
245
+ dimension_unit = serializers.CharField(
246
+ required=False, allow_null=True, allow_blank=True
247
+ )
248
+ weight_unit = serializers.CharField(
249
+ required=False, allow_null=True, allow_blank=True
250
+ )
251
+ currency = serializers.CharField(required=False, allow_null=True, allow_blank=True)
252
+
253
+ class Meta:
254
+ model = providers.ServiceLevel
255
+ exclude = ["created_at", "updated_at", "created_by"]
256
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
257
+
258
+ def update_zone(self, zone_index: int, zone_data: dict) -> None:
259
+ """Update a specific zone in the service level."""
260
+ if zone_index >= len(self.instance.zones):
261
+ raise exceptions.ValidationError(
262
+ _(f"Zone index {zone_index} is out of range"),
263
+ code="invalid_zone_index",
264
+ )
265
+
266
+ self.instance.zones[zone_index].update(
267
+ {k: v for k, v in zone_data.items() if v != strawberry.UNSET}
268
+ )
269
+ self.instance.save(update_fields=["zones"])
270
+
271
+ def update(self, instance, validated_data):
272
+ """Handle partial updates of service level data including zones."""
273
+ zones_data = validated_data.pop("zones", None)
274
+ instance = super().update(instance, validated_data)
275
+
276
+ if zones_data is not None:
277
+ # Handle zone updates if provided
278
+ existing_zones = instance.zones or []
279
+ updated_zones = []
280
+
281
+ for idx, zone_data in enumerate(zones_data):
282
+ if idx < len(existing_zones):
283
+ # Update existing zone
284
+ zone = existing_zones[idx].copy()
285
+ zone.update(
286
+ {k: v for k, v in zone_data.items() if v != strawberry.UNSET}
287
+ )
288
+ updated_zones.append(zone)
289
+ else:
290
+ # Add new zone
291
+ updated_zones.append(zone_data)
292
+
293
+ instance.zones = updated_zones
294
+ instance.save(update_fields=["zones"])
295
+
296
+ return instance
297
+
298
+
299
+ @serializers.owned_model_serializer
300
+ class LabelTemplateModelSerializer(serializers.ModelSerializer):
301
+ template_type = serializers.CharField(required=False)
302
+
303
+ class Meta:
304
+ model = providers.LabelTemplate
305
+ exclude = ["created_at", "updated_at", "created_by"]
306
+ extra_kwargs = {field: {"read_only": True} for field in ["id"]}
307
+
308
+
309
+ @serializers.owned_model_serializer
310
+ class RateSheetModelSerializer(serializers.ModelSerializer):
311
+ class Meta:
312
+ model = providers.RateSheet
313
+ exclude = ["created_at", "updated_at", "created_by"]
314
+ extra_kwargs = {field: {"read_only": True} for field in ["id", "services"]}
315
+
316
+ def update_services(
317
+ self, services_data: list, remove_missing: bool = False
318
+ ) -> None:
319
+ """Update services of the rate sheet."""
320
+ existing_services = {s.id: s for s in self.instance.services.all()}
321
+
322
+ for service_data in services_data:
323
+ service_id = service_data.get("id")
324
+ if service_id and service_id in existing_services:
325
+ # Update existing service
326
+ service = existing_services[service_id]
327
+ service_serializer = ServiceLevelModelSerializer(
328
+ service,
329
+ data=service_data,
330
+ context=self.context,
331
+ partial=True,
332
+ )
333
+ service_serializer.is_valid(raise_exception=True)
334
+ service_serializer.save()
335
+ else:
336
+ # Create new service
337
+ service_serializer = ServiceLevelModelSerializer(
338
+ data=service_data,
339
+ context=self.context,
340
+ )
341
+ service_serializer.is_valid(raise_exception=True)
342
+ service = service_serializer.save()
343
+ self.instance.services.add(service)
344
+
345
+ # Remove services that are not in the update
346
+ if remove_missing:
347
+ service_ids = {s.get("id") for s in services_data if "id" in s}
348
+ for service in existing_services.values():
349
+ if service.id not in service_ids:
350
+ self.instance.services.remove(service)
351
+ service.delete()
352
+
353
+ def update_carriers(self, carriers: list) -> None:
354
+ """Update carrier associations."""
355
+ if carriers is not None:
356
+ _ids = set(
357
+ [*carriers, *(self.instance.carriers.values_list("id", flat=True))]
358
+ )
359
+ _carriers = gateway.Carriers.list(
360
+ context=self.context,
361
+ carrier_name=self.instance.carrier_name,
362
+ ).filter(id__in=list(_ids))
363
+
364
+ for carrier in _carriers:
365
+ carrier.settings.rate_sheet = (
366
+ self.instance if carrier.id in carriers else None
367
+ )
368
+ carrier.settings.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
+ {{ link }}?token={{ token }}
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 *