karrio-server-documents 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.

Potentially problematic release.


This version of karrio-server-documents might be problematic. Click here for more details.

Files changed (36) hide show
  1. karrio/server/documents/__init__.py +0 -0
  2. karrio/server/documents/admin.py +93 -0
  3. karrio/server/documents/apps.py +13 -0
  4. karrio/server/documents/filters.py +14 -0
  5. karrio/server/documents/generator.py +240 -0
  6. karrio/server/documents/migrations/0001_initial.py +69 -0
  7. karrio/server/documents/migrations/0002_alter_documenttemplate_related_objects.py +18 -0
  8. karrio/server/documents/migrations/0003_rename_related_objects_documenttemplate_related_object.py +18 -0
  9. karrio/server/documents/migrations/0004_documenttemplate_active.py +18 -0
  10. karrio/server/documents/migrations/0005_alter_documenttemplate_description_and_more.py +23 -0
  11. karrio/server/documents/migrations/0006_documenttemplate_metadata.py +25 -0
  12. karrio/server/documents/migrations/0007_alter_documenttemplate_related_object.py +18 -0
  13. karrio/server/documents/migrations/0008_documenttemplate_options.py +26 -0
  14. karrio/server/documents/migrations/__init__.py +0 -0
  15. karrio/server/documents/models.py +61 -0
  16. karrio/server/documents/serializers/__init__.py +4 -0
  17. karrio/server/documents/serializers/base.py +89 -0
  18. karrio/server/documents/serializers/documents.py +9 -0
  19. karrio/server/documents/signals.py +31 -0
  20. karrio/server/documents/tests/__init__.py +6 -0
  21. karrio/server/documents/tests/test_generator.py +455 -0
  22. karrio/server/documents/tests/test_templates.py +318 -0
  23. karrio/server/documents/urls.py +11 -0
  24. karrio/server/documents/utils.py +2486 -0
  25. karrio/server/documents/views/__init__.py +0 -0
  26. karrio/server/documents/views/printers.py +233 -0
  27. karrio/server/documents/views/templates.py +216 -0
  28. karrio/server/graph/schemas/__init__.py +1 -0
  29. karrio/server/graph/schemas/documents/__init__.py +46 -0
  30. karrio/server/graph/schemas/documents/inputs.py +38 -0
  31. karrio/server/graph/schemas/documents/mutations.py +51 -0
  32. karrio/server/graph/schemas/documents/types.py +43 -0
  33. karrio_server_documents-2025.5rc1.dist-info/METADATA +19 -0
  34. karrio_server_documents-2025.5rc1.dist-info/RECORD +36 -0
  35. karrio_server_documents-2025.5rc1.dist-info/WHEEL +5 -0
  36. karrio_server_documents-2025.5rc1.dist-info/top_level.txt +2 -0
File without changes
@@ -0,0 +1,93 @@
1
+ import django.forms as forms
2
+ from django.contrib import admin
3
+ from django.conf import settings
4
+ from django.utils.translation import gettext_lazy as _
5
+ from django.utils.html import format_html
6
+ from django.urls import reverse
7
+
8
+ import karrio.server.documents.models as models
9
+ import karrio.server.documents.serializers.base as serializers
10
+
11
+
12
+ class DocumentTemplateAdminForm(forms.ModelForm):
13
+ class Meta:
14
+ model = models.DocumentTemplate
15
+ fields = "__all__"
16
+ widgets = {
17
+ "template": forms.Textarea(attrs={"rows": 30, "cols": 100}),
18
+ "related_object": forms.Select(
19
+ choices=[
20
+ (c.name, c.name) for c in list(serializers.TemplateRelatedObject)
21
+ ]
22
+ ),
23
+ }
24
+
25
+
26
+ class DocumentTemplateAdmin(admin.ModelAdmin):
27
+ form = DocumentTemplateAdminForm
28
+ list_display = ("slug", "name", "related_object", "active", "created_at", "preview_link")
29
+ search_fields = ("name", "description", "slug", "related_object")
30
+ list_filter = ("slug", "active")
31
+ readonly_fields = ("created_by", "full_preview_url")
32
+
33
+ def preview_link(self, obj):
34
+ """Return a clickable preview link that opens in a new tab"""
35
+ url = obj.preview_url()
36
+ return format_html(
37
+ '<a href="{}" target="_blank" rel="noopener noreferrer">Preview</a>',
38
+ url
39
+ )
40
+
41
+ preview_link.short_description = "Preview"
42
+
43
+ def full_preview_url(self, obj):
44
+ """Return the full absolute URL for the preview"""
45
+ if obj.pk:
46
+ request = self.request if hasattr(self, 'request') else None
47
+ if request:
48
+ relative_url = obj.preview_url()
49
+ absolute_url = request.build_absolute_uri(relative_url)
50
+ return format_html(
51
+ '<a href="{}" target="_blank" rel="noopener noreferrer">{}</a>',
52
+ absolute_url, absolute_url
53
+ )
54
+ else:
55
+ # Fallback if request is not available
56
+ relative_url = obj.preview_url()
57
+ return f"(Relative URL: {relative_url})"
58
+ return "Save the template first to generate preview URL"
59
+
60
+ full_preview_url.short_description = "Full Preview URL"
61
+
62
+ def get_queryset(self, request):
63
+ if settings.MULTI_ORGANIZATIONS:
64
+ return models.DocumentTemplate.objects.all().filter(
65
+ link__org__users__id=request.user.id
66
+ )
67
+
68
+ return super().get_queryset(request)
69
+
70
+ def save_model(self, request, obj, form, change):
71
+ # Store request for use in full_preview_url method
72
+ self.request = request
73
+ obj.created_by = request.user
74
+ super().save_model(request, obj, form, change)
75
+
76
+ if settings.MULTI_ORGANIZATIONS:
77
+ import karrio.server.serializers as serializers
78
+
79
+ serializers.link_org(obj, request)
80
+
81
+ def change_view(self, request, object_id, form_url='', extra_context=None):
82
+ # Store request for use in full_preview_url method
83
+ self.request = request
84
+ return super().change_view(request, object_id, form_url, extra_context)
85
+
86
+ def add_view(self, request, form_url='', extra_context=None):
87
+ # Store request for use in full_preview_url method
88
+ self.request = request
89
+ return super().add_view(request, form_url, extra_context)
90
+
91
+
92
+ # Register your models here.
93
+ admin.site.register(models.DocumentTemplate, DocumentTemplateAdmin)
@@ -0,0 +1,13 @@
1
+ from django.apps import AppConfig
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class DocumentsConfig(AppConfig):
6
+ name = "karrio.server.documents"
7
+ verbose_name = _("Documents")
8
+ default_auto_field = "django.db.models.BigAutoField"
9
+
10
+ def ready(self):
11
+ from karrio.server.documents import signals
12
+
13
+ signals.register_all()
@@ -0,0 +1,14 @@
1
+ import karrio.server.filters as filters
2
+ import karrio.server.documents.models as models
3
+
4
+
5
+ class DocumentTemplateFilter(filters.FilterSet):
6
+ name = filters.CharFilter(field_name="name", lookup_expr="icontains")
7
+ related_object = filters.CharFilter(
8
+ field_name="related_object", lookup_expr="icontains"
9
+ )
10
+ active = filters.BooleanFilter(field_name="active")
11
+
12
+ class Meta:
13
+ model = models.DocumentTemplate
14
+ fields: list = []
@@ -0,0 +1,240 @@
1
+ import io
2
+ import typing
3
+ import base64
4
+ import jinja2
5
+ import weasyprint
6
+ from django.db.models import Sum
7
+ import weasyprint.text.fonts as fonts
8
+
9
+ import karrio.lib as lib
10
+ import karrio.server.documents.utils as utils
11
+ import karrio.server.core.dataunits as dataunits
12
+ import karrio.server.orders.models as order_models
13
+ import karrio.server.manager.models as manager_models
14
+ import karrio.server.documents.models as document_models
15
+ import karrio.server.orders.serializers as orders_serializers
16
+ import karrio.server.manager.serializers as manager_serializers
17
+
18
+ FONT_CONFIG = fonts.FontConfiguration()
19
+ PAGE_SEPARATOR = '<p style="page-break-before: always"></p>'
20
+ STYLESHEETS = [
21
+ weasyprint.CSS(url="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css"),
22
+ weasyprint.CSS(
23
+ string="""
24
+ @page { margin: 1cm }
25
+ @font-face {
26
+ font-family: 'system';
27
+ src: local('Arial');
28
+ }
29
+ body { font-family: 'system', sans-serif; }
30
+ """
31
+ ),
32
+ ]
33
+ UNITS = {
34
+ "PAGE_SEPARATOR": PAGE_SEPARATOR,
35
+ "CountryISO": lib.units.CountryISO.as_dict(),
36
+ **dataunits.REFERENCE_MODELS,
37
+ }
38
+
39
+
40
+ class Documents:
41
+ @staticmethod
42
+ def generate(
43
+ template: str,
44
+ data: dict = {},
45
+ related_object: str = None,
46
+ **kwargs,
47
+ ) -> io.BytesIO:
48
+ options = kwargs.get("options") or {}
49
+ metadata = kwargs.get("metadata") or {}
50
+ shipment_contexts = data.get("shipments_context") or lib.identity(
51
+ get_shipments_context(data["shipments"])
52
+ if "shipments" in data and related_object == "shipment"
53
+ else []
54
+ )
55
+ order_contexts = data.get("orders_context") or lib.identity(
56
+ get_orders_context(data["orders"])
57
+ if "orders" in data and related_object == "order"
58
+ else []
59
+ )
60
+ generic_contexts = data.get("generic_context") or lib.identity(
61
+ [{"data": data}] if related_object is None else []
62
+ )
63
+ filename = lib.identity(
64
+ dict(filename=kwargs.get("doc_name")) if kwargs.get("doc_name") else {}
65
+ )
66
+
67
+ prefetch = lambda ctx: {
68
+ k: v
69
+ for o in lib.run_concurently(
70
+ lambda _: {
71
+ _[0]: str(
72
+ lib.failsafe(
73
+ lambda: _[1].render(
74
+ **ctx,
75
+ metadata=metadata,
76
+ units=UNITS,
77
+ utils=utils,
78
+ lib=lib,
79
+ )
80
+ )
81
+ or ""
82
+ )
83
+ },
84
+ [
85
+ (key, jinja2.Template(value))
86
+ for key, value in options.get("prefetch", {}).items()
87
+ ],
88
+ )
89
+ for k, v in o.items()
90
+ }
91
+
92
+ jinja_template = jinja2.Template(template)
93
+ all_contexts = shipment_contexts + order_contexts + generic_contexts
94
+ rendered_pages = lib.run_asynchronously(
95
+ lambda ctx: jinja_template.render(
96
+ **ctx,
97
+ metadata=metadata,
98
+ units=UNITS,
99
+ utils=utils,
100
+ lib=lib,
101
+ prefetch=prefetch(ctx),
102
+ ),
103
+ all_contexts,
104
+ )
105
+ content = PAGE_SEPARATOR.join(rendered_pages)
106
+
107
+ buffer = io.BytesIO()
108
+ html = weasyprint.HTML(string=content, encoding="utf-8")
109
+ html.write_pdf(
110
+ buffer,
111
+ stylesheets=STYLESHEETS,
112
+ font_config=FONT_CONFIG,
113
+ optimize_size=("fonts", "images"),
114
+ )
115
+
116
+ return buffer
117
+
118
+ @staticmethod
119
+ def generate_template(
120
+ document: document_models.DocumentTemplate,
121
+ data: dict,
122
+ **kwargs,
123
+ ) -> io.BytesIO:
124
+ return Documents.generate(
125
+ template=document.template,
126
+ data=data,
127
+ options=document.options,
128
+ metadata=document.metadata,
129
+ related_object=document.related_object,
130
+ **kwargs,
131
+ )
132
+
133
+ @staticmethod
134
+ def generate_shipment_document(slug: str, shipment, **kwargs) -> dict:
135
+ template = document_models.DocumentTemplate.objects.get(slug=slug)
136
+ carrier = kwargs.get("carrier") or getattr(
137
+ shipment, "selected_rate_carrier", None
138
+ )
139
+ params = dict(
140
+ shipments_context=[
141
+ dict(
142
+ shipment=manager_serializers.Shipment(shipment).data,
143
+ line_items=get_shipment_item_contexts(shipment),
144
+ carrier=get_carrier_context(carrier),
145
+ orders=orders_serializers.Order(
146
+ get_shipment_order_contexts(shipment),
147
+ many=True,
148
+ ).data,
149
+ )
150
+ ]
151
+ )
152
+ document = Documents.generate_template(template, params).getvalue()
153
+
154
+ return dict(
155
+ doc_format="PDF",
156
+ doc_name=f"{template.name}.pdf",
157
+ doc_type=(template.metadata or {}).get("doc_type") or "commercial_invoice",
158
+ doc_file=base64.b64encode(document).decode("utf-8"),
159
+ )
160
+
161
+
162
+ # -----------------------------------------------------------
163
+ # contexts data parsers
164
+ # -----------------------------------------------------------
165
+ # region
166
+
167
+
168
+ def get_shipments_context(shipment_ids: str) -> typing.List[dict]:
169
+ if shipment_ids == "sample":
170
+ return [utils.SHIPMENT_SAMPLE]
171
+
172
+ ids = shipment_ids.split(",")
173
+ shipments = manager_models.Shipment.objects.filter(id__in=ids)
174
+
175
+ return [
176
+ dict(
177
+ shipment=manager_serializers.Shipment(shipment).data,
178
+ line_items=get_shipment_item_contexts(shipment),
179
+ carrier=get_carrier_context(shipment.selected_rate_carrier),
180
+ orders=orders_serializers.Order(
181
+ get_shipment_order_contexts(shipment), many=True
182
+ ).data,
183
+ )
184
+ for shipment in shipments
185
+ ]
186
+
187
+
188
+ def get_shipment_item_contexts(shipment):
189
+ items = order_models.LineItem.objects.filter(
190
+ commodity_parcel__parcel_shipment=shipment
191
+ )
192
+ distinct_items = [
193
+ __ for _, __ in ({item.parent_id: item for item in items}).items()
194
+ ]
195
+
196
+ return [
197
+ {
198
+ **orders_serializers.LineItem(item.parent or item).data,
199
+ "ship_quantity": items.filter(parent_id=item.parent_id).aggregate(
200
+ Sum("quantity")
201
+ )["quantity__sum"],
202
+ "order": (orders_serializers.Order(item.order).data if item.order else {}),
203
+ }
204
+ for item in distinct_items
205
+ ]
206
+
207
+
208
+ def get_shipment_order_contexts(shipment):
209
+ return (
210
+ order_models.Order.objects.filter(
211
+ line_items__children__commodity_parcel__parcel_shipment=shipment
212
+ )
213
+ .order_by("-order_date")
214
+ .distinct()
215
+ )
216
+
217
+
218
+ def get_carrier_context(carrier=None):
219
+ if carrier is None:
220
+ return {}
221
+
222
+ return carrier.data.to_dict()
223
+
224
+
225
+ def get_orders_context(order_ids: str) -> typing.List[dict]:
226
+ if order_ids == "sample":
227
+ return [utils.ORDER_SAMPLE]
228
+
229
+ ids = order_ids.split(",")
230
+ orders = order_models.Order.objects.filter(id__in=ids)
231
+
232
+ return [
233
+ dict(
234
+ order=orders_serializers.Order(order).data,
235
+ )
236
+ for order in orders
237
+ ]
238
+
239
+
240
+ # endregion
@@ -0,0 +1,69 @@
1
+ # Generated by Django 3.2.11 on 2022-03-14 14:49
2
+
3
+ from django.conf import settings
4
+ import django.contrib.postgres.fields
5
+ import django.core.validators
6
+ from django.db import migrations, models
7
+ import django.db.models.deletion
8
+ import functools
9
+ import karrio.server.core.models.base
10
+
11
+
12
+ class Migration(migrations.Migration):
13
+
14
+ initial = True
15
+
16
+ dependencies = [
17
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
18
+ ]
19
+
20
+ operations = [
21
+ migrations.CreateModel(
22
+ name="DocumentTemplate",
23
+ fields=[
24
+ ("created_at", models.DateTimeField(auto_now_add=True)),
25
+ ("updated_at", models.DateTimeField(auto_now=True)),
26
+ (
27
+ "id",
28
+ models.CharField(
29
+ default=functools.partial(
30
+ karrio.server.core.models.base.uuid,
31
+ *(),
32
+ **{"prefix": "doc_"}
33
+ ),
34
+ editable=False,
35
+ max_length=50,
36
+ primary_key=True,
37
+ serialize=False,
38
+ ),
39
+ ),
40
+ (
41
+ "slug",
42
+ models.SlugField(
43
+ max_length=20,
44
+ validators=[
45
+ django.core.validators.RegexValidator("^[a-z0-9_]+$")
46
+ ],
47
+ ),
48
+ ),
49
+ ("name", models.CharField(max_length=50)),
50
+ ("template", models.TextField()),
51
+ ("description", models.CharField(blank=True, max_length=50, null=True)),
52
+ ("related_objects", models.JSONField(blank=True, null=True)),
53
+ (
54
+ "created_by",
55
+ models.ForeignKey(
56
+ on_delete=django.db.models.deletion.CASCADE,
57
+ to=settings.AUTH_USER_MODEL,
58
+ ),
59
+ ),
60
+ ],
61
+ options={
62
+ "verbose_name": "Document Template",
63
+ "verbose_name_plural": "Document Templates",
64
+ "db_table": "document-template",
65
+ "ordering": ["-created_at"],
66
+ },
67
+ bases=(karrio.server.core.models.base.ControlledAccessModel, models.Model),
68
+ ),
69
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 3.2.12 on 2022-03-21 20:47
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('documents', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='documenttemplate',
15
+ name='related_objects',
16
+ field=models.CharField(max_length=25),
17
+ ),
18
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 3.2.12 on 2022-03-21 20:47
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('documents', '0002_alter_documenttemplate_related_objects'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RenameField(
14
+ model_name='documenttemplate',
15
+ old_name='related_objects',
16
+ new_name='related_object',
17
+ ),
18
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 3.2.13 on 2022-06-18 09:09
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('documents', '0003_rename_related_objects_documenttemplate_related_object'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='documenttemplate',
15
+ name='active',
16
+ field=models.BooleanField(default=True, help_text='disable template flag. to filter out from active document downloads'),
17
+ ),
18
+ ]
@@ -0,0 +1,23 @@
1
+ # Generated by Django 4.1.3 on 2022-12-10 17:09
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("documents", "0004_documenttemplate_active"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name="documenttemplate",
15
+ name="description",
16
+ field=models.CharField(blank=True, db_index=True, max_length=50, null=True),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name="documenttemplate",
20
+ name="name",
21
+ field=models.CharField(db_index=True, max_length=50),
22
+ ),
23
+ ]
@@ -0,0 +1,25 @@
1
+ # Generated by Django 4.1.7 on 2023-02-22 15:35
2
+
3
+ from django.db import migrations, models
4
+ import functools
5
+ import karrio.server.core.models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ dependencies = [
10
+ ("documents", "0005_alter_documenttemplate_description_and_more"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name="documenttemplate",
16
+ name="metadata",
17
+ field=models.JSONField(
18
+ blank=True,
19
+ default=functools.partial(
20
+ karrio.server.core.models._identity, *(), **{"value": {}}
21
+ ),
22
+ null=True,
23
+ ),
24
+ ),
25
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 4.2.11 on 2024-05-27 17:53
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("documents", "0006_documenttemplate_metadata"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name="documenttemplate",
15
+ name="related_object",
16
+ field=models.CharField(blank=True, max_length=25, null=True),
17
+ ),
18
+ ]
@@ -0,0 +1,26 @@
1
+ # Generated by Django 4.2.16 on 2024-10-17 04:48
2
+
3
+ from django.db import migrations, models
4
+ import functools
5
+ import karrio.server.core.models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ("documents", "0007_alter_documenttemplate_related_object"),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AddField(
16
+ model_name="documenttemplate",
17
+ name="options",
18
+ field=models.JSONField(
19
+ blank=True,
20
+ default=functools.partial(
21
+ karrio.server.core.models._identity, *(), **{"value": {}}
22
+ ),
23
+ null=True,
24
+ ),
25
+ ),
26
+ ]
File without changes
@@ -0,0 +1,61 @@
1
+ import functools
2
+ import django.urls as urls
3
+ from django.db import models
4
+ from django.core.validators import RegexValidator
5
+
6
+ import karrio.server.core.models as core
7
+
8
+
9
+ @core.register_model
10
+ class DocumentTemplate(core.OwnedEntity):
11
+ class Meta:
12
+ db_table = "document-template"
13
+ verbose_name = "Document Template"
14
+ verbose_name_plural = "Document Templates"
15
+ ordering = ["-created_at"]
16
+
17
+ id = models.CharField(
18
+ max_length=50,
19
+ primary_key=True,
20
+ default=functools.partial(core.uuid, prefix="doc_"),
21
+ editable=False,
22
+ )
23
+ name = models.CharField(max_length=50, db_index=True)
24
+ slug = models.SlugField(
25
+ max_length=20,
26
+ validators=[RegexValidator(r"^[a-z0-9_]+$")],
27
+ db_index=True,
28
+ )
29
+ template = models.TextField()
30
+ description = models.CharField(
31
+ max_length=50,
32
+ null=True,
33
+ blank=True,
34
+ db_index=True,
35
+ )
36
+ related_object = models.CharField(max_length=25, blank=True, null=True)
37
+ active = models.BooleanField(
38
+ default=True,
39
+ help_text="disable template flag. to filter out from active document downloads",
40
+ )
41
+ metadata = models.JSONField(
42
+ blank=True,
43
+ null=True,
44
+ default=core.field_default({}),
45
+ )
46
+ options = models.JSONField(
47
+ blank=True,
48
+ null=True,
49
+ default=core.field_default({}),
50
+ )
51
+
52
+ @property
53
+ def object_type(self):
54
+ return "document-template"
55
+
56
+ @property
57
+ def preview_url(self):
58
+ return urls.reverse(
59
+ "karrio.server.documents:templates-documents-print",
60
+ kwargs=dict(pk=self.pk, slug=self.slug),
61
+ ) + f"?{self.related_object or 'shipment'}s=sample"
@@ -0,0 +1,4 @@
1
+ from karrio.server.serializers import *
2
+ from karrio.server.core.serializers import *
3
+ from karrio.server.documents.serializers.base import *
4
+ from karrio.server.documents.serializers.documents import *