linkml-store 0.1.8__py3-none-any.whl → 0.1.10__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 linkml-store might be problematic. Click here for more details.

Files changed (35) hide show
  1. linkml_store/api/client.py +15 -4
  2. linkml_store/api/collection.py +185 -15
  3. linkml_store/api/config.py +11 -3
  4. linkml_store/api/database.py +36 -5
  5. linkml_store/api/stores/duckdb/duckdb_collection.py +6 -3
  6. linkml_store/api/stores/duckdb/duckdb_database.py +20 -1
  7. linkml_store/api/stores/filesystem/__init__.py +7 -8
  8. linkml_store/api/stores/filesystem/filesystem_collection.py +150 -113
  9. linkml_store/api/stores/filesystem/filesystem_database.py +57 -21
  10. linkml_store/api/stores/mongodb/mongodb_collection.py +82 -34
  11. linkml_store/api/stores/mongodb/mongodb_database.py +13 -2
  12. linkml_store/api/types.py +4 -0
  13. linkml_store/cli.py +97 -8
  14. linkml_store/index/__init__.py +5 -3
  15. linkml_store/index/indexer.py +7 -2
  16. linkml_store/utils/change_utils.py +17 -0
  17. linkml_store/utils/format_utils.py +89 -8
  18. linkml_store/utils/patch_utils.py +126 -0
  19. linkml_store/utils/query_utils.py +89 -0
  20. linkml_store/utils/schema_utils.py +23 -0
  21. linkml_store/webapi/__init__.py +0 -0
  22. linkml_store/webapi/html/__init__.py +3 -0
  23. linkml_store/webapi/html/base.html.j2 +24 -0
  24. linkml_store/webapi/html/collection_details.html.j2 +15 -0
  25. linkml_store/webapi/html/database_details.html.j2 +16 -0
  26. linkml_store/webapi/html/databases.html.j2 +14 -0
  27. linkml_store/webapi/html/generic.html.j2 +46 -0
  28. linkml_store/webapi/main.py +572 -0
  29. linkml_store-0.1.10.dist-info/METADATA +138 -0
  30. linkml_store-0.1.10.dist-info/RECORD +58 -0
  31. {linkml_store-0.1.8.dist-info → linkml_store-0.1.10.dist-info}/entry_points.txt +1 -0
  32. linkml_store-0.1.8.dist-info/METADATA +0 -58
  33. linkml_store-0.1.8.dist-info/RECORD +0 -45
  34. {linkml_store-0.1.8.dist-info → linkml_store-0.1.10.dist-info}/LICENSE +0 -0
  35. {linkml_store-0.1.8.dist-info → linkml_store-0.1.10.dist-info}/WHEEL +0 -0
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}LinkML Store API{% endblock %}</title>
7
+ <style>
8
+ body { font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; }
9
+ h1 { color: #333; }
10
+ a { color: #0066cc; }
11
+ .navigation { margin-bottom: 20px; }
12
+ .content { margin-top: 20px; }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <div class="navigation">
17
+ <a href="/pages/">Home</a> |
18
+ <a href="/pages/databases">Databases</a>
19
+ </div>
20
+ <div class="content">
21
+ {% block content %}{% endblock %}
22
+ </div>
23
+ </body>
24
+ </html>
@@ -0,0 +1,15 @@
1
+ {% extends "base.html.j2" %}
2
+
3
+ {% block content %}
4
+ <h1>{{ response.meta.title }}</h1>
5
+ <p>Name: {{ params.collection_name }}</p>
6
+
7
+ <h2>Collections</h2>
8
+ <ul>
9
+ {% for collection in response.data.collections %}
10
+ <li>
11
+ <a href="/pages{{ collection.links|selectattr('rel', 'equalto', 'self')|first|attr('href') }}">{{ collection.name }}</a>
12
+ </li>
13
+ {% endfor %}
14
+ </ul>
15
+ {% endblock %}
@@ -0,0 +1,16 @@
1
+ {% extends "base.html.j2" %}
2
+
3
+ {% block content %}
4
+ <h1>{{ response.meta.title }}</h1>
5
+ <p>Handle: {{ response.data.handle }}</p>
6
+ <p>Number of collections: {{ response.data.num_collections }}</p>
7
+
8
+ <h2>Collections</h2>
9
+ <ul>
10
+ {% for collection in response.data.collections %}
11
+ <li>
12
+ <a href="/pages{{ collection.links|selectattr('rel', 'equalto', 'self')|first|attr('href') }}">{{ collection.name }}</a>
13
+ </li>
14
+ {% endfor %}
15
+ </ul>
16
+ {% endblock %}
@@ -0,0 +1,14 @@
1
+ {% extends "base.html.j2" %}
2
+
3
+ {% block title %}LinkML Store API - Databases{% endblock %}
4
+
5
+ {% block content %}
6
+ <h1>Databases</h1>
7
+ <ul>
8
+ {% for db in response.data.databases %}
9
+ <li>
10
+ <a href="/pages/databases/{{ db.name }}">{{ db.name }}</a>
11
+ </li>
12
+ {% endfor %}
13
+ </ul>
14
+ {% endblock %}
@@ -0,0 +1,46 @@
1
+ {% extends "base.html.j2" %}
2
+
3
+ {% block title %}{meta.path}{% endblock %}
4
+
5
+ {% block content %}
6
+ <h1>Meta</h1>
7
+ <pre>
8
+ {{ response.meta }}
9
+ </pre>
10
+
11
+ <h1>Links</h1>
12
+ <ul>
13
+ {% for link in response.links %}
14
+ <li>
15
+ <a href="/pages{{ link.href }}">{{ link.rel }} ({{ link.href }})</a>
16
+ </li>
17
+ {% endfor %}
18
+ </ul>
19
+ </ul>
20
+
21
+ <h1>Data</h1>
22
+ {% if data_html %}
23
+ <ul>
24
+ {% for e in data_html %}
25
+ <li>{{ e|safe }}</li>
26
+ {% endfor %}
27
+ </ul>
28
+ {% else %}
29
+
30
+ {% if "items" in response.data %}
31
+ <ul>
32
+ {% for item in response.data['items'] %}
33
+ <li>
34
+ {{ item.name }}
35
+ {% for link in item.links %}
36
+ <a href="/pages{{ link.href }}">{{ link.rel }}</a>
37
+ {% endfor %}
38
+ </li>
39
+ {% endfor %}
40
+ </ul>
41
+ {% endif %}
42
+ <pre>
43
+ {{ response.data }}
44
+ </pre>
45
+ {% endif %}
46
+ {% endblock %}
@@ -0,0 +1,572 @@
1
+ import os
2
+ import uuid
3
+ from datetime import datetime
4
+ from typing import Any, Callable, Dict, List, Optional
5
+
6
+ import httpx
7
+ import uvicorn
8
+ import yaml
9
+ from dotenv import load_dotenv
10
+ from fastapi import Depends, FastAPI, HTTPException, Query, Request
11
+ from fastapi.responses import HTMLResponse
12
+ from fastapi.templating import Jinja2Templates
13
+ from linkml_renderer.renderers.html_renderer import HTMLRenderer
14
+ from linkml_renderer.style.style_engine import StyleEngine
15
+ from linkml_runtime.dumpers import json_dumper
16
+ from pydantic import BaseModel, Field
17
+ from starlette.responses import JSONResponse
18
+
19
+ from linkml_store import Client
20
+ from linkml_store.api import Collection, Database
21
+ from linkml_store.api.queries import Query as StoreQuery
22
+ from linkml_store.utils.format_utils import load_objects
23
+ from linkml_store.webapi.html import HTML_TEMPLATES_DIR
24
+
25
+ html_renderer = HTMLRenderer()
26
+
27
+ # Load environment variables from .env file
28
+ load_dotenv()
29
+
30
+ # Parse command-line arguments
31
+ # parser = argparse.ArgumentParser(description="LinkML Store FastAPI server")
32
+ # parser.add_argument("--config", type=str, help="Path to the configuration file")
33
+ # args = parser.parse_args()
34
+
35
+
36
+ # Load configuration
37
+ config = None
38
+ if os.environ.get("LINKML_STORE_CONFIG"):
39
+ with open(os.environ["LINKML_STORE_CONFIG"], "r") as f:
40
+ config = yaml.safe_load(f)
41
+
42
+ # Initialize client
43
+ client = Client().from_config(config) if config else Client()
44
+
45
+ app = FastAPI(title="LinkML Store API")
46
+
47
+ templates = Jinja2Templates(directory=HTML_TEMPLATES_DIR)
48
+
49
+
50
+ # Pydantic models for requests and responses
51
+
52
+
53
+ class Link(BaseModel):
54
+ rel: str
55
+ href: str
56
+
57
+
58
+ class Meta(BaseModel):
59
+ path: Optional[str] = None
60
+ path_template: Optional[str] = None
61
+ params: Dict[str, Any] = {}
62
+ timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
63
+ version: str = "1.0"
64
+ request_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
65
+
66
+
67
+ class Error(BaseModel):
68
+ code: str
69
+ message: str
70
+ details: Optional[str] = None
71
+
72
+
73
+ class APIResponse(BaseModel):
74
+ data: Optional[Any] = None
75
+ meta: Meta = Field(default_factory=Meta)
76
+ links: Optional[List[Link]] = None
77
+ errors: Optional[List[Error]] = None
78
+
79
+
80
+ class DatabaseCreate(BaseModel):
81
+ name: str
82
+ handle: str
83
+
84
+
85
+ class CollectionCreate(BaseModel):
86
+ name: str
87
+ alias: Optional[str] = None
88
+
89
+
90
+ class ObjectInsert(BaseModel):
91
+ objects: List[Dict[str, Any]]
92
+
93
+
94
+ # Helper functions
95
+
96
+
97
+ def get_client():
98
+ return client
99
+
100
+
101
+ def get_database(client: Client = Depends(get_client)):
102
+ def _get_database(database_name: str) -> Database:
103
+ try:
104
+ return client.get_database(database_name)
105
+ except KeyError:
106
+ raise HTTPException(status_code=404, detail=f"Database '{database_name}' not found")
107
+
108
+ return _get_database
109
+
110
+
111
+ def get_collection(database: Database = Depends(get_database())):
112
+ def _get_collection(collection_name: str) -> Collection:
113
+ try:
114
+ return database.get_collection(collection_name)
115
+ except KeyError:
116
+ raise HTTPException(status_code=404, detail=f"Collection '{collection_name}' not found")
117
+
118
+ return _get_collection
119
+
120
+
121
+ # API routes
122
+
123
+
124
+ @app.get("/", response_model=APIResponse, description="Top level.")
125
+ async def top(request: Request, client: Client = Depends(get_client)):
126
+ links = [
127
+ Link(rel="self", href="/"),
128
+ Link(rel="docs", href="/docs"),
129
+ Link(rel="databases", href="/databases"),
130
+ Link(rel="config", href="/config"),
131
+ ]
132
+ return APIResponse(data={}, links=links)
133
+
134
+
135
+ @app.get("/config", response_model=APIResponse, description="Configuration metadata")
136
+ async def config(request: Request, client: Client = Depends(get_client)):
137
+ client = get_client()
138
+ data = client.metadata
139
+ links = [
140
+ Link(rel="self", href="/"),
141
+ Link(rel="docs", href="/docs"),
142
+ Link(rel="databases", href="/databases"),
143
+ Link(rel="config", href="/config"),
144
+ ]
145
+ return APIResponse(data=data, links=links)
146
+
147
+
148
+ @app.get(
149
+ "/databases", response_model=APIResponse, description="List all databases with clickable links to their details."
150
+ )
151
+ async def list_databases(request: Request, client: Client = Depends(get_client)):
152
+ databases = list(client.databases.keys())
153
+
154
+ database_links = [Link(rel="database", href=f"/databases/{db_name}") for db_name in databases]
155
+
156
+ additional_links = [
157
+ Link(rel="self", href="/databases"),
158
+ Link(rel="create_database", href="/database/create"),
159
+ ]
160
+
161
+ response_data = {
162
+ "objects": [
163
+ {
164
+ "name": db_name,
165
+ "type": "Database",
166
+ "links": [
167
+ {"rel": "self", "href": f"/databases/{db_name}"},
168
+ {"rel": "collections", "href": f"/databases/{db_name}/collections"},
169
+ {"rel": "schema", "href": f"/databases/{db_name}/schema"},
170
+ ],
171
+ }
172
+ for db_name in databases
173
+ ]
174
+ }
175
+
176
+ api_response = APIResponse(
177
+ meta=Meta(path=request.url.path, path_template="databases", params={}),
178
+ data=response_data,
179
+ links=additional_links + database_links,
180
+ )
181
+ if request.headers.get("Accept") == "text/html":
182
+ return templates.TemplateResponse("databases.html", {"request": request, "response": api_response})
183
+ else:
184
+ return JSONResponse(content=api_response.dict())
185
+
186
+
187
+ @app.post("/database/create", response_model=APIResponse)
188
+ async def create_database(database: DatabaseCreate, client: Client = Depends(get_client)):
189
+ db = client.attach_database(database.handle, alias=database.name)
190
+ return APIResponse(
191
+ data={"name": db.metadata.alias, "handle": db.metadata.handle},
192
+ links=[
193
+ Link(rel="self", href=f"/databases/{db.metadata.alias}"),
194
+ Link(rel="collections", href=f"/databases/{db.metadata.alias}/collections"),
195
+ ],
196
+ )
197
+
198
+
199
+ @app.get("/databases/{database_name}", response_model=APIResponse)
200
+ async def get_database_details(
201
+ request: Request, database_name: str, get_db: Callable[[str], Database] = Depends(get_database)
202
+ ):
203
+ database = get_db(database_name)
204
+ collections = database.list_collections()
205
+ return APIResponse(
206
+ meta=Meta(path=request.url.path, path_template="database_details", params={"database_name": database_name}),
207
+ data={
208
+ "name": database.metadata.alias,
209
+ "handle": database.metadata.handle,
210
+ "num_collections": len(collections),
211
+ "collections": [
212
+ {
213
+ "name": c.name,
214
+ "links": [
215
+ {"rel": "self", "href": f"/databases/{database_name}/collections/{c.name}"},
216
+ {"rel": "objects", "href": f"/databases/{database_name}/collections/{c.name}/objects"},
217
+ ],
218
+ }
219
+ for c in collections
220
+ ],
221
+ },
222
+ links=[
223
+ Link(rel="self", href=f"/databases/{database_name}"),
224
+ Link(rel="collections", href=f"/databases/{database_name}/collections"),
225
+ Link(rel="schema", href=f"/databases/{database_name}/schema"),
226
+ Link(rel="all_databases", href="/databases"),
227
+ ],
228
+ )
229
+
230
+
231
+ @app.get("/databases/{database_name}/collections", response_model=APIResponse)
232
+ async def list_collections(
233
+ request: Request, database_name: str, get_db: Callable[[str], Database] = Depends(get_database)
234
+ ):
235
+ database = get_db(database_name)
236
+ collections = database.list_collections()
237
+ return APIResponse(
238
+ meta=Meta(path=request.url.path, path_template="collections", params={}),
239
+ data={
240
+ "items": [
241
+ {
242
+ "name": c.name,
243
+ "type": "Collection",
244
+ "links": [
245
+ {"rel": "self", "href": f"/databases/{database_name}/collections/{c.name}"},
246
+ {"rel": "objects", "href": f"/databases/{database_name}/collections/{c.name}/objects"},
247
+ ],
248
+ }
249
+ for c in collections
250
+ ]
251
+ },
252
+ links=[
253
+ Link(rel="self", href=f"/databases/{database_name}/collections"),
254
+ Link(rel="database", href=f"/databases/{database_name}"),
255
+ Link(rel="create_collection", href=f"/databases/{database_name}/collections"),
256
+ ],
257
+ )
258
+
259
+
260
+ @app.get("/databases/{database_name}/collections/{collection_name}", response_model=APIResponse)
261
+ async def get_collection_details(
262
+ request: Request,
263
+ database_name: str,
264
+ collection_name: str,
265
+ get_db: Callable[[str], Database] = Depends(get_database),
266
+ ):
267
+ database = get_db(database_name)
268
+ collection = database.get_collection(collection_name)
269
+ return APIResponse(
270
+ meta=Meta(
271
+ path=request.url.path,
272
+ path_template="collection_details",
273
+ params={
274
+ "database_name": database_name,
275
+ "collection_name": collection_name,
276
+ },
277
+ ),
278
+ data={"name": collection.name, "alias": collection.alias, "num_objects": collection.find({}).num_rows},
279
+ links=[
280
+ Link(rel="self", href=f"/databases/{database_name}/collections/{collection_name}"),
281
+ Link(rel="objects", href=f"/databases/{database_name}/collections/{collection_name}/objects"),
282
+ Link(rel="facets", href=f"/databases/{database_name}/collections/{collection_name}/facets"),
283
+ Link(rel="search", href=f"/databases/{database_name}/collections/{collection_name}/search/{{term}}"),
284
+ Link(rel="database", href=f"/databases/{database_name}"),
285
+ ],
286
+ )
287
+
288
+
289
+ @app.post("/databases/{database_name}/collections/{collection_name}/create", response_model=APIResponse)
290
+ async def create_collection(
291
+ request: Request,
292
+ database_name: str,
293
+ collection: CollectionCreate,
294
+ get_db: Callable[[str], Database] = Depends(get_database),
295
+ ):
296
+ database = get_db(database_name)
297
+ new_collection = database.create_collection(collection.name, alias=collection.alias)
298
+ return APIResponse(
299
+ meta=Meta(path=request.url.path, path_template="create_collection", params={"database_name": database_name}),
300
+ data={"name": new_collection.name, "alias": new_collection.alias},
301
+ links=[
302
+ Link(rel="self", href=f"/databases/{database_name}/collections/{new_collection.name}"),
303
+ Link(rel="database", href=f"/databases/{database_name}"),
304
+ Link(rel="objects", href=f"/databases/{database_name}/collections/{new_collection.name}/objects"),
305
+ Link(rel="objects", href=f"/databases/{database_name}/collections/{new_collection.name}/facets"),
306
+ ],
307
+ )
308
+
309
+
310
+ @app.get("/databases/{database_name}/collections/{collection_name}/objects", response_model=APIResponse)
311
+ async def collection_list_objects(
312
+ request: Request,
313
+ database_name: str,
314
+ collection_name: str,
315
+ where: Optional[str] = None,
316
+ limit: int = Query(10, ge=1, le=100),
317
+ offset: int = Query(0, ge=0),
318
+ get_db: Callable[[str], Database] = Depends(get_database),
319
+ ):
320
+ database = get_db(database_name)
321
+ collection = database.get_collection(collection_name)
322
+ where_clause = load_objects(where) if where else None
323
+ query = StoreQuery(from_table=collection.name, where_clause=where_clause, limit=limit, offset=offset)
324
+ result = collection.query(query)
325
+
326
+ total_count = collection.find({}).num_rows
327
+ total_pages = (total_count + limit - 1) // limit
328
+ current_page = offset // limit + 1
329
+
330
+ base_url = f"/databases/{database_name}/collections/{collection_name}/objects"
331
+ links = [Link(rel="self", href=f"{base_url}?limit={limit}&offset={offset}")]
332
+ if current_page > 1:
333
+ links.append(Link(rel="prev", href=f"{base_url}?limit={limit}&offset={offset - limit}"))
334
+ if current_page < total_pages:
335
+ links.append(Link(rel="next", href=f"{base_url}?limit={limit}&offset={offset + limit}"))
336
+
337
+ links.extend(
338
+ [
339
+ Link(rel="first", href=f"{base_url}?limit={limit}&offset=0"),
340
+ Link(rel="last", href=f"{base_url}?limit={limit}&offset={(total_pages - 1) * limit}"),
341
+ Link(rel="collection", href=f"/databases/{database_name}/collections/{collection_name}"),
342
+ Link(rel="database", href=f"/databases/{database_name}"),
343
+ ]
344
+ )
345
+
346
+ return APIResponse(
347
+ meta=Meta(
348
+ path=request.url.path,
349
+ path_template="objects",
350
+ params={
351
+ "database_name": database_name,
352
+ "collection_name": collection_name,
353
+ },
354
+ ),
355
+ data={
356
+ "domain_objects": result.rows,
357
+ "total_count": total_count,
358
+ "page": current_page,
359
+ "total_pages": total_pages,
360
+ "page_size": limit,
361
+ },
362
+ links=links,
363
+ )
364
+
365
+
366
+ @app.get("/databases/{database_name}/collections/{collection_name}/search/{term}", response_model=APIResponse)
367
+ async def search_objects(
368
+ request: Request,
369
+ database_name: str,
370
+ collection_name: str,
371
+ term: str,
372
+ limit: int = Query(5, ge=1, le=100),
373
+ offset: int = Query(0, ge=0),
374
+ get_db: Callable[[str], Database] = Depends(get_database),
375
+ ):
376
+ database = get_db(database_name)
377
+ collection = database.get_collection(collection_name)
378
+ result = collection.search(term, limit=limit)
379
+
380
+ total_count = result.num_rows
381
+ total_pages = (total_count + limit - 1) // limit
382
+ current_page = offset // limit + 1
383
+
384
+ base_url = f"/databases/{database_name}/collections/{collection_name}/search/{term}"
385
+ links = [Link(rel="self", href=f"{base_url}?limit={limit}&offset={offset}")]
386
+ if current_page > 1:
387
+ links.append(Link(rel="prev", href=f"{base_url}?limit={limit}&offset={offset - limit}"))
388
+ if current_page < total_pages:
389
+ links.append(Link(rel="next", href=f"{base_url}?limit={limit}&offset={offset + limit}"))
390
+
391
+ links.extend(
392
+ [
393
+ Link(rel="first", href=f"{base_url}?limit={limit}&offset=0"),
394
+ Link(rel="last", href=f"{base_url}?limit={limit}&offset={(total_pages - 1) * limit}"),
395
+ Link(rel="collection", href=f"/databases/{database_name}/collections/{collection_name}"),
396
+ Link(rel="database", href=f"/databases/{database_name}"),
397
+ ]
398
+ )
399
+
400
+ return APIResponse(
401
+ meta=Meta(
402
+ path=request.url.path,
403
+ path_template="collection_search",
404
+ params={
405
+ "database_name": database_name,
406
+ "collection_name": collection_name,
407
+ "term": term,
408
+ },
409
+ ),
410
+ data={
411
+ "objects": result.ranked_rows,
412
+ "total_count": total_count,
413
+ "page": current_page,
414
+ "total_pages": total_pages,
415
+ "page_size": limit,
416
+ },
417
+ links=links,
418
+ )
419
+
420
+
421
+ @app.get("/databases/{database_name}/collections/{collection_name}/facets", response_model=APIResponse)
422
+ async def objects_facets(
423
+ request: Request,
424
+ database_name: str,
425
+ collection_name: str,
426
+ where: Optional[str] = None,
427
+ limit: int = Query(10, ge=1, le=100),
428
+ offset: int = Query(0, ge=0),
429
+ get_db: Callable[[str], Database] = Depends(get_database),
430
+ ):
431
+ database = get_db(database_name)
432
+ collection = database.get_collection(collection_name)
433
+ where_clause = load_objects(where) if where else None
434
+ results = collection.query_facets(where_clause)
435
+
436
+ total_count = collection.find({}).num_rows
437
+ total_pages = (total_count + limit - 1) // limit
438
+ current_page = offset // limit + 1
439
+
440
+ base_url = f"/databases/{database_name}/collections/{collection_name}/facets"
441
+ links = [Link(rel="self", href=f"{base_url}?limit={limit}&offset={offset}")]
442
+ if current_page > 1:
443
+ links.append(Link(rel="prev", href=f"{base_url}?limit={limit}&offset={offset - limit}"))
444
+ if current_page < total_pages:
445
+ links.append(Link(rel="next", href=f"{base_url}?limit={limit}&offset={offset + limit}"))
446
+
447
+ links.extend(
448
+ [
449
+ Link(rel="first", href=f"{base_url}?limit={limit}&offset=0"),
450
+ Link(rel="last", href=f"{base_url}?limit={limit}&offset={(total_pages - 1) * limit}"),
451
+ Link(rel="collection", href=f"/databases/{database_name}/collections/{collection_name}"),
452
+ Link(rel="database", href=f"/databases/{database_name}"),
453
+ ]
454
+ )
455
+
456
+ return APIResponse(
457
+ meta=Meta(
458
+ path=request.url.path,
459
+ path_template="facets",
460
+ params={
461
+ "database_name": database_name,
462
+ "collection_name": collection_name,
463
+ },
464
+ ),
465
+ data={
466
+ "items": results,
467
+ "total_count": total_count,
468
+ "page": current_page,
469
+ "total_pages": total_pages,
470
+ "page_size": limit,
471
+ },
472
+ links=links,
473
+ )
474
+
475
+
476
+ @app.post("/databases/{database_name}/collections/{collection_name}/objects", response_model=APIResponse)
477
+ async def insert_objects(
478
+ database_name: str,
479
+ collection_name: str,
480
+ insert: ObjectInsert,
481
+ get_db: Callable[[str], Database] = Depends(get_database),
482
+ ):
483
+ database = get_db(database_name)
484
+ collection = database.get_collection(collection_name)
485
+ collection.insert(insert.objects)
486
+ return APIResponse(
487
+ data={"inserted_count": len(insert.objects)},
488
+ links=[
489
+ Link(rel="self", href=f"/databases/{database_name}/collections/{collection_name}/objects"),
490
+ Link(rel="collection", href=f"/databases/{database_name}/collections/{collection_name}"),
491
+ ],
492
+ )
493
+
494
+
495
+ @app.get("/databases/{database_name}/schema", response_model=APIResponse)
496
+ async def get_database_schema(
497
+ request: Request, database_name: str, get_db: Callable[[str], Database] = Depends(get_database)
498
+ ):
499
+ database = get_db(database_name)
500
+ schema = database.schema_view.schema
501
+ schema_dict = json_dumper.to_dict(schema)
502
+ return APIResponse(
503
+ meta=Meta(path=request.url.path, path_template="schema", params={"database_name": database_name}),
504
+ data={"schema": schema_dict},
505
+ links=[
506
+ Link(rel="self", href=f"/databases/{database_name}/schema"),
507
+ Link(rel="database", href=f"/databases/{database_name}"),
508
+ ],
509
+ )
510
+
511
+
512
+ @app.get("/pages/{path:path}", response_class=HTMLResponse)
513
+ async def generic_page(request: Request, path: str, get_db: Callable[[str], Database] = Depends(get_database)):
514
+ # Construct the API URL
515
+ api_url = f"{request.base_url}{path}"
516
+ query_params = dict(request.query_params)
517
+
518
+ # Make a request to the API
519
+ async with httpx.AsyncClient() as client:
520
+ response = await client.get(api_url, params=query_params)
521
+
522
+ if response.status_code != 200:
523
+ raise HTTPException(status_code=response.status_code, detail=response.text)
524
+
525
+ # Parse the JSON response
526
+ api_data = response.json()
527
+
528
+ # Use the template specified in the API response
529
+ meta = api_data["meta"]
530
+ # template_name = meta['path_template'] + ".html.j2"
531
+ template_name = "generic.html.j2"
532
+ params = meta["params"]
533
+
534
+ data = api_data["data"]
535
+ data_html = None
536
+ if "domain_objects" in data:
537
+ objs = data["domain_objects"]
538
+ db = get_db(params["database_name"])
539
+ sv = db.schema_view
540
+ collection = db.get_collection(params["collection_name"])
541
+ if collection.class_definition():
542
+ cn = collection.class_definition().name
543
+ style_engine = StyleEngine(schemaview=sv)
544
+ html_renderer.style_engine = style_engine
545
+ data_html = [html_renderer.render(obj, schemaview=sv, source_element_name=cn) for obj in objs]
546
+
547
+ # Render the appropriate template
548
+ return templates.TemplateResponse(
549
+ template_name,
550
+ {
551
+ "request": request,
552
+ "response": api_data,
553
+ "current_path": f"/pages/{path}",
554
+ "data_html": data_html,
555
+ "params": params,
556
+ },
557
+ )
558
+
559
+
560
+ def run_server():
561
+ import uvicorn
562
+
563
+ uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
564
+
565
+
566
+ # if __name__ == "__main__":
567
+ # run_server()
568
+
569
+
570
+ def start():
571
+ """Launched with `poetry run start` at root level"""
572
+ uvicorn.run("linkml_store.webapi.main:app", host="127.0.0.1", port=8000, reload=True)