apis-data-projection 0.1.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 (25) hide show
  1. apis_data_projection-0.1.0/.github/workflows/publish.yml +51 -0
  2. apis_data_projection-0.1.0/.gitignore +79 -0
  3. apis_data_projection-0.1.0/.python-version +1 -0
  4. apis_data_projection-0.1.0/LICENSE +21 -0
  5. apis_data_projection-0.1.0/PKG-INFO +27 -0
  6. apis_data_projection-0.1.0/README.md +11 -0
  7. apis_data_projection-0.1.0/apis_data_projection/__init__.py +0 -0
  8. apis_data_projection-0.1.0/apis_data_projection/admin.py +43 -0
  9. apis_data_projection-0.1.0/apis_data_projection/apps.py +7 -0
  10. apis_data_projection-0.1.0/apis_data_projection/checks.py +47 -0
  11. apis_data_projection-0.1.0/apis_data_projection/config.py +4 -0
  12. apis_data_projection-0.1.0/apis_data_projection/management/commands/rebuild_projections.py +16 -0
  13. apis_data_projection-0.1.0/apis_data_projection/migrations/0001_initial.py +311 -0
  14. apis_data_projection-0.1.0/apis_data_projection/migrations/__init__.py +0 -0
  15. apis_data_projection-0.1.0/apis_data_projection/models.py +184 -0
  16. apis_data_projection-0.1.0/apis_data_projection/projection_sync.py +867 -0
  17. apis_data_projection-0.1.0/apis_data_projection/templates/admin/apis_data_projection/change_list.html +10 -0
  18. apis_data_projection-0.1.0/manage.py +16 -0
  19. apis_data_projection-0.1.0/pyproject.toml +21 -0
  20. apis_data_projection-0.1.0/sample_project/migrations/0001_initial.py +341 -0
  21. apis_data_projection-0.1.0/sample_project/migrations/__init__.py +0 -0
  22. apis_data_projection-0.1.0/sample_project/models.py +95 -0
  23. apis_data_projection-0.1.0/sample_project/settings.py +18 -0
  24. apis_data_projection-0.1.0/sample_project/urls.py +3 -0
  25. apis_data_projection-0.1.0/uv.lock +1210 -0
@@ -0,0 +1,51 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push
5
+
6
+ jobs:
7
+ build:
8
+ name: Build distribution
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ with:
13
+ persist-credentials: false
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.x"
18
+ - name: Install pypa/build
19
+ run: >-
20
+ python3 -m
21
+ pip install
22
+ build
23
+ --user
24
+ - name: Build a binary wheel and a source tarball
25
+ run: python3 -m build
26
+ - name: Store the distribution packages
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: python-package-distributions
30
+ path: dist/
31
+
32
+ publish-to-pypi:
33
+ name: >-
34
+ Publish Python distribution to PyPI
35
+ if: startsWith(github.ref, 'refs/tags/')
36
+ needs:
37
+ - build
38
+ runs-on: ubuntu-latest
39
+ environment:
40
+ name: pypi
41
+ url: https://pypi.org/project/apis-data-projection/
42
+ permissions:
43
+ id-token: write
44
+ steps:
45
+ - name: Download all the dists
46
+ uses: actions/download-artifact@v4
47
+ with:
48
+ name: python-package-distributions
49
+ path: dist/
50
+ - name: Publish distribution to PyPI
51
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,79 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ *.pyo
5
+ build/
6
+ dist/
7
+ wheels/
8
+ *.egg-info
9
+ *.egg
10
+ .eggs/
11
+ MANIFEST
12
+
13
+ # Virtual environments
14
+ .venv
15
+ venv/
16
+ env/
17
+ ENV/
18
+ .env
19
+
20
+ # Environment variables
21
+ .env.local
22
+ .env.*.local
23
+
24
+ # Distribution / packaging
25
+ *.tar.gz
26
+ *.whl
27
+
28
+ # Testing
29
+ .tox/
30
+ .nox/
31
+ .coverage
32
+ .coverage.*
33
+ coverage.xml
34
+ htmlcov/
35
+ .pytest_cache/
36
+ .hypothesis/
37
+
38
+ # Type checking
39
+ .mypy_cache/
40
+ .dmypy.json
41
+ .pytype/
42
+ .pyre/
43
+
44
+ # Linting / formatting
45
+ .ruff_cache/
46
+
47
+ # Django
48
+ *.log
49
+ local_settings.py
50
+ db.sqlite3
51
+ db.sqlite3-journal
52
+ media/
53
+ staticfiles/
54
+ static/
55
+
56
+ # Django migrations (optional — remove if you want to track migrations)
57
+ # */migrations/*.py
58
+ # !*/migrations/__init__.py
59
+
60
+ # Celery
61
+ celerybeat-schedule
62
+ celerybeat.pid
63
+
64
+ # IDE / editors
65
+ .idea/
66
+ .vscode/
67
+ *.swp
68
+ *.swo
69
+ *~
70
+ .DS_Store
71
+
72
+ # Jupyter Notebooks
73
+ .ipynb_checkpoints/
74
+ *.ipynb
75
+
76
+ # uv / pip
77
+ pip-log.txt
78
+ pip-delete-this-directory.txt
79
+ .uv/
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Saranya Balasubramanian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: apis-data-projection
3
+ Version: 0.1.0
4
+ Summary: Data projection layer for APIS
5
+ Project-URL: Homepage, https://github.com/gythaogg/apis-data-projection
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: data projection
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.13
13
+ Requires-Dist: apis-acdhch-default-settings>=2.20.0
14
+ Requires-Dist: apis-core-rdf>=0.64.5
15
+ Description-Content-Type: text/markdown
16
+
17
+ # apis-data-projection
18
+
19
+ ## Aim
20
+ This package creates projections of APIS data models in order to
21
+ - support fast universal search over entities and relations
22
+ - create relation aware representation of entities. For eg. if there is "AA lives in XX relationship" then the representation of AA will contain "lives in XX" and the representation of XX will contain "residence of AA"
23
+ - to support faceting and filtering by datetime
24
+ - to provide an efficient relation layer for graph representations
25
+
26
+
27
+
@@ -0,0 +1,11 @@
1
+ # apis-data-projection
2
+
3
+ ## Aim
4
+ This package creates projections of APIS data models in order to
5
+ - support fast universal search over entities and relations
6
+ - create relation aware representation of entities. For eg. if there is "AA lives in XX relationship" then the representation of AA will contain "lives in XX" and the representation of XX will contain "residence of AA"
7
+ - to support faceting and filtering by datetime
8
+ - to provide an efficient relation layer for graph representations
9
+
10
+
11
+
@@ -0,0 +1,43 @@
1
+ from apis_data_projection.models import RelationProjection, EntityProjection, EntityFacet
2
+ from django.contrib import admin, messages
3
+ from django.http import HttpResponseRedirect
4
+ from django.urls import path
5
+ from django.template.response import TemplateResponse
6
+
7
+ from .models import EntityProjection
8
+ from .projection_sync import ProjectionSyncService
9
+ from apis_core.entities.abc import Entity
10
+ from apis_core.relations.models import Relation
11
+
12
+ @admin.register(RelationProjection)
13
+ class RelationProjectionAdmin(admin.ModelAdmin):
14
+ list_display = tuple(field.name for field in RelationProjection._meta.fields)
15
+
16
+ @admin.register(EntityFacet)
17
+ class EntityFacetAdmin(admin.ModelAdmin):
18
+ list_display = tuple(field.name for field in EntityFacet._meta.fields)
19
+
20
+
21
+ @admin.register(EntityProjection)
22
+ class EntityProjectionAdmin(admin.ModelAdmin):
23
+ list_display = [field.name for field in EntityProjection._meta.fields]
24
+
25
+ def get_urls(self):
26
+ urls = super().get_urls()
27
+ custom_urls = [
28
+ path(
29
+ "rebuild-projections/",
30
+ self.admin_site.admin_view(self.rebuild_projections_view),
31
+ name="apis_data_projection_rebuild_projections",
32
+ ),
33
+ ]
34
+ return custom_urls + urls
35
+
36
+ def rebuild_projections_view(self, request):
37
+ service = ProjectionSyncService(
38
+ abstract_entity_model=Entity,
39
+ relation_model=Relation,
40
+ )
41
+ service.sync_all()
42
+ self.message_user(request, "Projection rebuild completed.", level=messages.SUCCESS)
43
+ return HttpResponseRedirect("../")
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ApisDataProjectionConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "apis_data_projection"
7
+ verbose_name = "APIS Data Projection"
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from django.core.checks import Error, Warning, register
4
+
5
+
6
+
7
+ def _has_attr(model, attr_name: str) -> bool:
8
+ try:
9
+ model._meta.get_field(attr_name)
10
+ return True
11
+ except Exception:
12
+ return hasattr(model, attr_name)
13
+
14
+
15
+ @register()
16
+ def apis_data_projection_checks(app_configs, **kwargs):
17
+ errors = []
18
+
19
+ try:
20
+ from apis_core.entities.abc import Entity
21
+ except Exception as exc:
22
+ errors.append(
23
+ Error(
24
+ "apis_core.entities.abc.Entity could not be imported.",
25
+ hint="Install and configure apis_core before using apis_data_projection.",
26
+ id="apis_data_projection.E001",
27
+ obj=exc,
28
+ )
29
+ )
30
+ Entity = None
31
+
32
+ try:
33
+ from apis_core.relations import Relation
34
+ except Exception as exc:
35
+ errors.append(
36
+ Error(
37
+ "apis_core.relations.Relation could not be imported.",
38
+ hint="Install and configure apis_core before using apis_data_projection.",
39
+ id="apis_data_projection.E002",
40
+ obj=exc,
41
+ )
42
+ )
43
+ Relation = None
44
+
45
+ if Entity is None or Relation is None:
46
+ return errors
47
+
@@ -0,0 +1,4 @@
1
+ DEFAULT_FACET_CONFIG: dict[str, set[str]] = {
2
+ "entity_excluded_fields": set(),
3
+ "relation_excluded_fields": set(),
4
+ }
@@ -0,0 +1,16 @@
1
+ from django.core.management.base import BaseCommand
2
+
3
+ from apis_data_projection.projection_sync import ProjectionSyncService
4
+ from apis_core.entities.abc import Entity
5
+ from apis_core.relations.models import Relation
6
+
7
+ class Command(BaseCommand):
8
+ help = "Rebuild entity and relation projections."
9
+
10
+ def handle(self, *args, **options):
11
+ service = ProjectionSyncService(
12
+ abstract_entity_model=Entity,
13
+ relation_model=Relation,
14
+ )
15
+ service.sync_all()
16
+ self.stdout.write(self.style.SUCCESS("Projection rebuild completed."))
@@ -0,0 +1,311 @@
1
+ # Generated by Django 6.0.6 on 2026-06-19 09:38
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ("contenttypes", "0002_remove_content_type_name"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name="EntityProjection",
18
+ fields=[
19
+ (
20
+ "id",
21
+ models.BigAutoField(
22
+ auto_created=True,
23
+ primary_key=True,
24
+ serialize=False,
25
+ verbose_name="ID",
26
+ ),
27
+ ),
28
+ ("source_object_id", models.PositiveIntegerField()),
29
+ (
30
+ "entity_type",
31
+ models.CharField(
32
+ db_index=True,
33
+ help_text="Entity class name, e.g. Person, Place, Group.",
34
+ max_length=100,
35
+ ),
36
+ ),
37
+ ("label", models.CharField(db_index=True, max_length=255)),
38
+ ("uri", models.CharField(blank=True, max_length=500)),
39
+ ("search_text", models.TextField(blank=True)),
40
+ ("start_date", models.DateField(blank=True, null=True)),
41
+ ("end_date", models.DateField(blank=True, null=True)),
42
+ (
43
+ "properties_json",
44
+ models.JSONField(
45
+ blank=True,
46
+ default=dict,
47
+ help_text="dict representation of the entity's properties.",
48
+ ),
49
+ ),
50
+ (
51
+ "relations_json",
52
+ models.JSONField(
53
+ blank=True,
54
+ default=dict,
55
+ help_text="dict representation of the entity's relations.",
56
+ ),
57
+ ),
58
+ ("created_at", models.DateTimeField(auto_now_add=True)),
59
+ ("updated_at", models.DateTimeField(auto_now=True)),
60
+ (
61
+ "source_content_type",
62
+ models.ForeignKey(
63
+ on_delete=django.db.models.deletion.CASCADE,
64
+ related_name="apis_data_projection_entity_projections",
65
+ to="contenttypes.contenttype",
66
+ ),
67
+ ),
68
+ ],
69
+ options={
70
+ "ordering": ["label"],
71
+ },
72
+ ),
73
+ migrations.CreateModel(
74
+ name="EntityFacet",
75
+ fields=[
76
+ (
77
+ "id",
78
+ models.BigAutoField(
79
+ auto_created=True,
80
+ primary_key=True,
81
+ serialize=False,
82
+ verbose_name="ID",
83
+ ),
84
+ ),
85
+ (
86
+ "name",
87
+ models.CharField(
88
+ db_index=True,
89
+ help_text="Facet key, e.g. profession, gender, works in, place of residence.",
90
+ max_length=100,
91
+ ),
92
+ ),
93
+ (
94
+ "value",
95
+ models.CharField(
96
+ db_index=True,
97
+ help_text="Facet display value, e.g. Wizard, male, Ankh Morpork.",
98
+ max_length=255,
99
+ ),
100
+ ),
101
+ (
102
+ "source",
103
+ models.CharField(
104
+ blank=True,
105
+ help_text="Optional origin, e.g. profession, gender, relation:works in.",
106
+ max_length=255,
107
+ ),
108
+ ),
109
+ (
110
+ "kind",
111
+ models.CharField(
112
+ blank=True,
113
+ help_text="Optional classifier, e.g. scalar, fk, m2m, relation-derived.",
114
+ max_length=30,
115
+ ),
116
+ ),
117
+ (
118
+ "weight",
119
+ models.DecimalField(
120
+ blank=True, decimal_places=2, max_digits=6, null=True
121
+ ),
122
+ ),
123
+ (
124
+ "extra",
125
+ models.JSONField(
126
+ blank=True,
127
+ default=dict,
128
+ help_text="Optional extra facet metadata",
129
+ ),
130
+ ),
131
+ ("created_at", models.DateTimeField(auto_now_add=True)),
132
+ ("updated_at", models.DateTimeField(auto_now=True)),
133
+ (
134
+ "entity_projection_id",
135
+ models.ForeignKey(
136
+ on_delete=django.db.models.deletion.CASCADE,
137
+ related_name="facets",
138
+ to="apis_data_projection.entityprojection",
139
+ ),
140
+ ),
141
+ ],
142
+ options={
143
+ "ordering": ["name", "value"],
144
+ },
145
+ ),
146
+ migrations.CreateModel(
147
+ name="RelationProjection",
148
+ fields=[
149
+ (
150
+ "id",
151
+ models.BigAutoField(
152
+ auto_created=True,
153
+ primary_key=True,
154
+ serialize=False,
155
+ verbose_name="ID",
156
+ ),
157
+ ),
158
+ ("source_relation_object_id", models.PositiveIntegerField()),
159
+ ("subj_object_id", models.PositiveIntegerField()),
160
+ ("obj_object_id", models.PositiveIntegerField()),
161
+ ("subj_label", models.CharField(blank=True, max_length=255)),
162
+ ("obj_label", models.CharField(blank=True, max_length=255)),
163
+ ("subj_entity_type", models.CharField(db_index=True, max_length=100)),
164
+ ("obj_entity_type", models.CharField(db_index=True, max_length=100)),
165
+ (
166
+ "relation_type",
167
+ models.CharField(
168
+ db_index=True, help_text="Relation class name", max_length=100
169
+ ),
170
+ ),
171
+ ("forward_label", models.CharField(db_index=True, max_length=255)),
172
+ (
173
+ "reverse_label",
174
+ models.CharField(blank=True, db_index=True, max_length=255),
175
+ ),
176
+ ("search_text", models.TextField(blank=True)),
177
+ ("start_date", models.DateField(blank=True, null=True)),
178
+ ("end_date", models.DateField(blank=True, null=True)),
179
+ (
180
+ "properties_json",
181
+ models.JSONField(
182
+ blank=True,
183
+ default=dict,
184
+ help_text="Optional extra relation metadata that is not a first-class column.",
185
+ ),
186
+ ),
187
+ ("created_at", models.DateTimeField(auto_now_add=True)),
188
+ ("updated_at", models.DateTimeField(auto_now=True)),
189
+ (
190
+ "obj_content_type",
191
+ models.ForeignKey(
192
+ on_delete=django.db.models.deletion.CASCADE,
193
+ related_name="apis_data_projection_relation_obj",
194
+ to="contenttypes.contenttype",
195
+ ),
196
+ ),
197
+ (
198
+ "source_relation_content_type",
199
+ models.ForeignKey(
200
+ on_delete=django.db.models.deletion.CASCADE,
201
+ related_name="apis_data_projection_relation_projections",
202
+ to="contenttypes.contenttype",
203
+ ),
204
+ ),
205
+ (
206
+ "subj_content_type",
207
+ models.ForeignKey(
208
+ on_delete=django.db.models.deletion.CASCADE,
209
+ related_name="apis_data_projection_relation_subj",
210
+ to="contenttypes.contenttype",
211
+ ),
212
+ ),
213
+ ],
214
+ options={
215
+ "ordering": ["relation_type", "subj_label", "obj_label"],
216
+ },
217
+ ),
218
+ migrations.AddIndex(
219
+ model_name="entityprojection",
220
+ index=models.Index(
221
+ fields=["source_content_type", "source_object_id"],
222
+ name="apis_data_p_source__ae8955_idx",
223
+ ),
224
+ ),
225
+ migrations.AddIndex(
226
+ model_name="entityprojection",
227
+ index=models.Index(
228
+ fields=["entity_type"], name="apis_data_p_entity__a87a6d_idx"
229
+ ),
230
+ ),
231
+ migrations.AddIndex(
232
+ model_name="entityprojection",
233
+ index=models.Index(fields=["label"], name="apis_data_p_label_493c50_idx"),
234
+ ),
235
+ migrations.AddConstraint(
236
+ model_name="entityprojection",
237
+ constraint=models.UniqueConstraint(
238
+ fields=("source_content_type", "source_object_id"),
239
+ name="uniq_entity_projection_source",
240
+ ),
241
+ ),
242
+ migrations.AddIndex(
243
+ model_name="entityfacet",
244
+ index=models.Index(
245
+ fields=["entity_projection_id"], name="apis_data_p_entity__3eec92_idx"
246
+ ),
247
+ ),
248
+ migrations.AddIndex(
249
+ model_name="entityfacet",
250
+ index=models.Index(
251
+ fields=["name", "value"], name="apis_data_p_name_a349f0_idx"
252
+ ),
253
+ ),
254
+ migrations.AddIndex(
255
+ model_name="entityfacet",
256
+ index=models.Index(fields=["name"], name="apis_data_p_name_bd2449_idx"),
257
+ ),
258
+ migrations.AddConstraint(
259
+ model_name="entityfacet",
260
+ constraint=models.UniqueConstraint(
261
+ fields=("entity_projection_id", "name", "value"),
262
+ name="uniq_entity_facet_per_projection",
263
+ ),
264
+ ),
265
+ migrations.AddIndex(
266
+ model_name="relationprojection",
267
+ index=models.Index(
268
+ fields=["source_relation_content_type", "source_relation_object_id"],
269
+ name="apis_data_p_source__2a9bae_idx",
270
+ ),
271
+ ),
272
+ migrations.AddIndex(
273
+ model_name="relationprojection",
274
+ index=models.Index(
275
+ fields=["subj_content_type", "subj_object_id"],
276
+ name="apis_data_p_subj_co_0bd7df_idx",
277
+ ),
278
+ ),
279
+ migrations.AddIndex(
280
+ model_name="relationprojection",
281
+ index=models.Index(
282
+ fields=["obj_content_type", "obj_object_id"],
283
+ name="apis_data_p_obj_con_255e1e_idx",
284
+ ),
285
+ ),
286
+ migrations.AddIndex(
287
+ model_name="relationprojection",
288
+ index=models.Index(
289
+ fields=["relation_type"], name="apis_data_p_relatio_ede626_idx"
290
+ ),
291
+ ),
292
+ migrations.AddIndex(
293
+ model_name="relationprojection",
294
+ index=models.Index(
295
+ fields=["forward_label"], name="apis_data_p_forward_384d6d_idx"
296
+ ),
297
+ ),
298
+ migrations.AddIndex(
299
+ model_name="relationprojection",
300
+ index=models.Index(
301
+ fields=["reverse_label"], name="apis_data_p_reverse_5ef6f2_idx"
302
+ ),
303
+ ),
304
+ migrations.AddConstraint(
305
+ model_name="relationprojection",
306
+ constraint=models.UniqueConstraint(
307
+ fields=("source_relation_content_type", "source_relation_object_id"),
308
+ name="uniq_relation_projection_source",
309
+ ),
310
+ ),
311
+ ]