linkml-store 0.3.0__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 (101) hide show
  1. linkml_store/__init__.py +7 -0
  2. linkml_store/api/__init__.py +8 -0
  3. linkml_store/api/client.py +414 -0
  4. linkml_store/api/collection.py +1280 -0
  5. linkml_store/api/config.py +187 -0
  6. linkml_store/api/database.py +862 -0
  7. linkml_store/api/queries.py +69 -0
  8. linkml_store/api/stores/__init__.py +0 -0
  9. linkml_store/api/stores/chromadb/__init__.py +7 -0
  10. linkml_store/api/stores/chromadb/chromadb_collection.py +121 -0
  11. linkml_store/api/stores/chromadb/chromadb_database.py +89 -0
  12. linkml_store/api/stores/dremio/__init__.py +10 -0
  13. linkml_store/api/stores/dremio/dremio_collection.py +555 -0
  14. linkml_store/api/stores/dremio/dremio_database.py +1052 -0
  15. linkml_store/api/stores/dremio/mappings.py +105 -0
  16. linkml_store/api/stores/dremio_rest/__init__.py +11 -0
  17. linkml_store/api/stores/dremio_rest/dremio_rest_collection.py +502 -0
  18. linkml_store/api/stores/dremio_rest/dremio_rest_database.py +1023 -0
  19. linkml_store/api/stores/duckdb/__init__.py +16 -0
  20. linkml_store/api/stores/duckdb/duckdb_collection.py +339 -0
  21. linkml_store/api/stores/duckdb/duckdb_database.py +283 -0
  22. linkml_store/api/stores/duckdb/mappings.py +8 -0
  23. linkml_store/api/stores/filesystem/__init__.py +15 -0
  24. linkml_store/api/stores/filesystem/filesystem_collection.py +186 -0
  25. linkml_store/api/stores/filesystem/filesystem_database.py +81 -0
  26. linkml_store/api/stores/hdf5/__init__.py +7 -0
  27. linkml_store/api/stores/hdf5/hdf5_collection.py +104 -0
  28. linkml_store/api/stores/hdf5/hdf5_database.py +79 -0
  29. linkml_store/api/stores/ibis/__init__.py +5 -0
  30. linkml_store/api/stores/ibis/ibis_collection.py +488 -0
  31. linkml_store/api/stores/ibis/ibis_database.py +328 -0
  32. linkml_store/api/stores/mongodb/__init__.py +25 -0
  33. linkml_store/api/stores/mongodb/mongodb_collection.py +379 -0
  34. linkml_store/api/stores/mongodb/mongodb_database.py +114 -0
  35. linkml_store/api/stores/neo4j/__init__.py +0 -0
  36. linkml_store/api/stores/neo4j/neo4j_collection.py +429 -0
  37. linkml_store/api/stores/neo4j/neo4j_database.py +154 -0
  38. linkml_store/api/stores/solr/__init__.py +3 -0
  39. linkml_store/api/stores/solr/solr_collection.py +224 -0
  40. linkml_store/api/stores/solr/solr_database.py +83 -0
  41. linkml_store/api/stores/solr/solr_utils.py +0 -0
  42. linkml_store/api/types.py +4 -0
  43. linkml_store/cli.py +1147 -0
  44. linkml_store/constants.py +7 -0
  45. linkml_store/graphs/__init__.py +0 -0
  46. linkml_store/graphs/graph_map.py +24 -0
  47. linkml_store/index/__init__.py +53 -0
  48. linkml_store/index/implementations/__init__.py +0 -0
  49. linkml_store/index/implementations/llm_indexer.py +174 -0
  50. linkml_store/index/implementations/simple_indexer.py +43 -0
  51. linkml_store/index/indexer.py +211 -0
  52. linkml_store/inference/__init__.py +13 -0
  53. linkml_store/inference/evaluation.py +195 -0
  54. linkml_store/inference/implementations/__init__.py +0 -0
  55. linkml_store/inference/implementations/llm_inference_engine.py +154 -0
  56. linkml_store/inference/implementations/rag_inference_engine.py +276 -0
  57. linkml_store/inference/implementations/rule_based_inference_engine.py +169 -0
  58. linkml_store/inference/implementations/sklearn_inference_engine.py +314 -0
  59. linkml_store/inference/inference_config.py +66 -0
  60. linkml_store/inference/inference_engine.py +209 -0
  61. linkml_store/inference/inference_engine_registry.py +74 -0
  62. linkml_store/plotting/__init__.py +5 -0
  63. linkml_store/plotting/cli.py +826 -0
  64. linkml_store/plotting/dimensionality_reduction.py +453 -0
  65. linkml_store/plotting/embedding_plot.py +489 -0
  66. linkml_store/plotting/facet_chart.py +73 -0
  67. linkml_store/plotting/heatmap.py +383 -0
  68. linkml_store/utils/__init__.py +0 -0
  69. linkml_store/utils/change_utils.py +17 -0
  70. linkml_store/utils/dat_parser.py +95 -0
  71. linkml_store/utils/embedding_matcher.py +424 -0
  72. linkml_store/utils/embedding_utils.py +299 -0
  73. linkml_store/utils/enrichment_analyzer.py +217 -0
  74. linkml_store/utils/file_utils.py +37 -0
  75. linkml_store/utils/format_utils.py +550 -0
  76. linkml_store/utils/io.py +38 -0
  77. linkml_store/utils/llm_utils.py +122 -0
  78. linkml_store/utils/mongodb_utils.py +145 -0
  79. linkml_store/utils/neo4j_utils.py +42 -0
  80. linkml_store/utils/object_utils.py +190 -0
  81. linkml_store/utils/pandas_utils.py +93 -0
  82. linkml_store/utils/patch_utils.py +126 -0
  83. linkml_store/utils/query_utils.py +89 -0
  84. linkml_store/utils/schema_utils.py +23 -0
  85. linkml_store/utils/sklearn_utils.py +193 -0
  86. linkml_store/utils/sql_utils.py +177 -0
  87. linkml_store/utils/stats_utils.py +53 -0
  88. linkml_store/utils/vector_utils.py +158 -0
  89. linkml_store/webapi/__init__.py +0 -0
  90. linkml_store/webapi/html/__init__.py +3 -0
  91. linkml_store/webapi/html/base.html.j2 +24 -0
  92. linkml_store/webapi/html/collection_details.html.j2 +15 -0
  93. linkml_store/webapi/html/database_details.html.j2 +16 -0
  94. linkml_store/webapi/html/databases.html.j2 +14 -0
  95. linkml_store/webapi/html/generic.html.j2 +43 -0
  96. linkml_store/webapi/main.py +855 -0
  97. linkml_store-0.3.0.dist-info/METADATA +226 -0
  98. linkml_store-0.3.0.dist-info/RECORD +101 -0
  99. linkml_store-0.3.0.dist-info/WHEEL +4 -0
  100. linkml_store-0.3.0.dist-info/entry_points.txt +3 -0
  101. linkml_store-0.3.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,855 @@
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 Item(BaseModel):
59
+ name: str
60
+ type: Optional[str] = None
61
+ links: List[Link]
62
+ data: Optional[Any] = None
63
+ html: Optional[str] = None
64
+
65
+
66
+ class ItemType(BaseModel):
67
+ name: str
68
+ description: Optional[str] = None
69
+
70
+
71
+ class Meta(BaseModel):
72
+ path: Optional[str] = None
73
+ path_template: Optional[str] = None
74
+ params: Dict[str, Any] = {}
75
+ timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
76
+ version: str = "1.0"
77
+ request_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
78
+ item_count: Optional[int] = None
79
+ paged: bool = False
80
+ page: int = 1
81
+ page_size: Optional[int] = None
82
+
83
+
84
+ class Error(BaseModel):
85
+ code: str
86
+ message: str
87
+ details: Optional[str] = None
88
+
89
+
90
+ class APIResponse(BaseModel):
91
+ meta: Meta = Field(default_factory=Meta)
92
+ items: Optional[List[Item]] = None
93
+ item_type: Optional[ItemType] = None
94
+ data: Optional[Any] = None
95
+ links: Optional[List[Link]] = None
96
+ errors: Optional[List[Error]] = None
97
+
98
+
99
+ class DatabaseCreate(BaseModel):
100
+ name: str
101
+ handle: str
102
+
103
+
104
+ class CollectionCreate(BaseModel):
105
+ name: str
106
+ alias: Optional[str] = None
107
+
108
+
109
+ class ObjectInsert(BaseModel):
110
+ objects: List[Dict[str, Any]]
111
+
112
+
113
+ # Helper functions
114
+
115
+
116
+ def get_client():
117
+ return client
118
+
119
+
120
+ def get_database(client: Client = Depends(get_client)):
121
+ def _get_database(database_name: str) -> Database:
122
+ try:
123
+ return client.get_database(database_name)
124
+ except KeyError:
125
+ raise HTTPException(status_code=404, detail=f"Database '{database_name}' not found")
126
+
127
+ return _get_database
128
+
129
+
130
+ def get_collection(database: Database = Depends(get_database())):
131
+ def _get_collection(collection_name: str) -> Collection:
132
+ try:
133
+ return database.get_collection(collection_name)
134
+ except KeyError:
135
+ raise HTTPException(status_code=404, detail=f"Collection '{collection_name}' not found")
136
+
137
+ return _get_collection
138
+
139
+
140
+ # API routes
141
+
142
+
143
+ @app.get("/", response_model=APIResponse, description="Top level.")
144
+ async def top(request: Request, client: Client = Depends(get_client)):
145
+ links = [
146
+ Link(rel="self", href="/"),
147
+ Link(rel="docs", href="/docs"),
148
+ Link(rel="pages", href="/pages"),
149
+ Link(rel="databases", href="/databases"),
150
+ Link(rel="config", href="/config"),
151
+ ]
152
+ return APIResponse(data={}, links=links)
153
+
154
+
155
+ @app.get("/config", response_model=APIResponse, description="Configuration metadata")
156
+ async def config(request: Request, client: Client = Depends(get_client)):
157
+ client = get_client()
158
+ data = client.metadata
159
+ links = [
160
+ Link(rel="self", href="/config"),
161
+ Link(rel="parent", href="/"),
162
+ ]
163
+ return APIResponse(data=data, links=links)
164
+
165
+
166
+ @app.get(
167
+ "/databases", response_model=APIResponse, description="List all databases with clickable links to their details."
168
+ )
169
+ async def list_databases(request: Request, client: Client = Depends(get_client)):
170
+ databases = list(client.databases.keys())
171
+
172
+ # database_links = [Link(rel="database", href=f"/databases/{db_name}") for db_name in databases]
173
+ database_links = []
174
+
175
+ additional_links = [
176
+ Link(rel="self", href="/databases"),
177
+ Link(rel="parent", href="/"),
178
+ Link(rel="create_database", href="/database/create"),
179
+ ]
180
+
181
+ items = [
182
+ Item(name=db_name, type="Database", links=[Link(rel="self", href=f"/databases/{db_name}")], data={})
183
+ for db_name in databases
184
+ ]
185
+
186
+ api_response = APIResponse(
187
+ meta=Meta(path=request.url.path, path_template="databases", params={}),
188
+ data={},
189
+ items=items,
190
+ links=additional_links + database_links,
191
+ )
192
+ if request.headers.get("Accept") == "text/html":
193
+ return templates.TemplateResponse("databases.html", {"request": request, "response": api_response})
194
+ else:
195
+ return JSONResponse(content=api_response.dict())
196
+
197
+
198
+ @app.post("/database/create", response_model=APIResponse)
199
+ async def create_database(database: DatabaseCreate, client: Client = Depends(get_client)):
200
+ # TODO
201
+ db = client.attach_database(database.handle, alias=database.name)
202
+ return APIResponse(
203
+ data={"name": db.metadata.alias, "handle": db.metadata.handle},
204
+ links=[
205
+ Link(rel="self", href=f"/databases/{db.metadata.alias}"),
206
+ Link(rel="collections", href=f"/databases/{db.metadata.alias}/collections"),
207
+ ],
208
+ )
209
+
210
+
211
+ @app.get("/databases/{database_name}", response_model=APIResponse)
212
+ async def get_database_details(
213
+ request: Request, database_name: str, get_db: Callable[[str], Database] = Depends(get_database)
214
+ ):
215
+ database = get_db(database_name)
216
+ collections = database.list_collections()
217
+ db_metadata = database.metadata.model_dump(exclude_none=True, exclude_defaults=True)
218
+ if "collections" in db_metadata:
219
+ # do not replicate information
220
+ del db_metadata["collections"]
221
+ return APIResponse(
222
+ meta=Meta(path=request.url.path, path_template="database_details", params={"database_name": database_name}),
223
+ data={
224
+ "name": database.metadata.alias,
225
+ "handle": database.metadata.handle,
226
+ "config": db_metadata,
227
+ "num_collections": len(collections),
228
+ },
229
+ links=[
230
+ Link(rel="self", href=f"/databases/{database_name}"),
231
+ Link(rel="collections", href=f"/databases/{database_name}/collections"),
232
+ Link(rel="schema", href=f"/databases/{database_name}/schema"),
233
+ Link(rel="parent", href="/databases"),
234
+ ],
235
+ )
236
+
237
+
238
+ @app.get("/databases/{database_name}/collections", response_model=APIResponse)
239
+ async def list_database_collections(
240
+ request: Request, database_name: str, get_db: Callable[[str], Database] = Depends(get_database)
241
+ ):
242
+ database = get_db(database_name)
243
+ collections = database.list_collections()
244
+ items = [
245
+ Item(
246
+ name=c.alias,
247
+ type="Collection",
248
+ links=[
249
+ Link(rel="self", href=f"/databases/{database_name}/collections/{c.alias}"),
250
+ Link(rel="objects", href=f"/databases/{database_name}/collections/{c.alias}/objects"),
251
+ ],
252
+ )
253
+ for c in collections
254
+ ]
255
+ return APIResponse(
256
+ meta=Meta(path=request.url.path, path_template="collections", params={}),
257
+ items=items,
258
+ data={},
259
+ links=[
260
+ Link(rel="self", href=f"/databases/{database_name}/collections"),
261
+ Link(rel="database", href=f"/databases/{database_name}"),
262
+ Link(rel="create_collection", href=f"/databases/{database_name}/collections"),
263
+ ],
264
+ )
265
+
266
+
267
+ @app.get("/databases/{database_name}/collections/{collection_name}", response_model=APIResponse)
268
+ async def get_collection_details(
269
+ request: Request,
270
+ database_name: str,
271
+ collection_name: str,
272
+ get_db: Callable[[str], Database] = Depends(get_database),
273
+ ):
274
+ database = get_db(database_name)
275
+ collection = database.get_collection(collection_name)
276
+ return APIResponse(
277
+ meta=Meta(
278
+ path=request.url.path,
279
+ path_template="collection_details",
280
+ params={
281
+ "database_name": database_name,
282
+ "collection_name": collection_name,
283
+ },
284
+ ),
285
+ data={
286
+ "target_class_name": collection.target_class_name,
287
+ "alias": collection.alias,
288
+ "num_objects": collection.find({}).num_rows,
289
+ },
290
+ links=[
291
+ Link(rel="self", href=f"/databases/{database_name}/collections/{collection_name}"),
292
+ Link(rel="objects", href=f"/databases/{database_name}/collections/{collection_name}/objects"),
293
+ Link(rel="attributes", href=f"/databases/{database_name}/collections/{collection_name}/attributes"),
294
+ Link(rel="search", href=f"/databases/{database_name}/collections/{collection_name}/search/{{term}}"),
295
+ Link(rel="database", href=f"/databases/{database_name}"),
296
+ ],
297
+ )
298
+
299
+
300
+ @app.post("/databases/{database_name}/collections/{collection_name}/create", response_model=APIResponse)
301
+ async def create_collection(
302
+ request: Request,
303
+ database_name: str,
304
+ collection: CollectionCreate,
305
+ get_db: Callable[[str], Database] = Depends(get_database),
306
+ ):
307
+ database = get_db(database_name)
308
+ new_collection = database.create_collection(collection.alias, alias=collection.alias)
309
+ return APIResponse(
310
+ meta=Meta(path=request.url.path, path_template="create_collection", params={"database_name": database_name}),
311
+ data={"name": new_collection.alias, "alias": new_collection.alias},
312
+ links=[
313
+ Link(rel="self", href=f"/databases/{database_name}/collections/{new_collection.alias}"),
314
+ Link(rel="database", href=f"/databases/{database_name}"),
315
+ Link(rel="objects", href=f"/databases/{database_name}/collections/{new_collection.alias}/objects"),
316
+ Link(rel="objects", href=f"/databases/{database_name}/collections/{new_collection.alias}/facets"),
317
+ ],
318
+ )
319
+
320
+
321
+ @app.get("/databases/{database_name}/collections/{collection_name}/objects", response_model=APIResponse)
322
+ async def list_collection_objects(
323
+ request: Request,
324
+ database_name: str,
325
+ collection_name: str,
326
+ where: Optional[str] = None,
327
+ limit: int = Query(10, ge=1, le=100),
328
+ offset: int = Query(0, ge=0),
329
+ get_db: Callable[[str], Database] = Depends(get_database),
330
+ ):
331
+ database = get_db(database_name)
332
+ collection = database.get_collection(collection_name)
333
+ where_clause = load_objects(where) if where else None
334
+ query = StoreQuery(from_table=collection.alias, where_clause=where_clause, limit=limit, offset=offset)
335
+ result = collection.query(query)
336
+ base_url = f"/databases/{database_name}/collections/{collection_name}/objects"
337
+
338
+ items = []
339
+ cd = collection.class_definition()
340
+ id_att_name = collection.identifier_attribute_name
341
+ for i, row in enumerate(result.rows):
342
+ if id_att_name:
343
+ name = row[id_att_name]
344
+ link = Link(rel="self", href=f"{base_url}/{name}")
345
+ else:
346
+ ix = offset + i
347
+ name = str(ix)
348
+ link = Link(rel="self", href=f"{base_url}_index/{ix}")
349
+ item = Item(name=name, data=row, links=[link])
350
+ items.append(item)
351
+
352
+ total_count = collection.find({}).num_rows
353
+ total_pages = (total_count + limit - 1) // limit
354
+ current_page = offset // limit + 1
355
+
356
+ links = [Link(rel="self", href=f"{base_url}?limit={limit}&offset={offset}")]
357
+ if current_page > 1:
358
+ links.append(Link(rel="prev", href=f"{base_url}?limit={limit}&offset={offset - limit}"))
359
+ if current_page < total_pages:
360
+ links.append(Link(rel="next", href=f"{base_url}?limit={limit}&offset={offset + limit}"))
361
+
362
+ links.extend(
363
+ [
364
+ Link(rel="first", href=f"{base_url}?limit={limit}&offset=0"),
365
+ Link(rel="last", href=f"{base_url}?limit={limit}&offset={(total_pages - 1) * limit}"),
366
+ Link(rel="parent", href=f"/databases/{database_name}/collections/{collection_name}"),
367
+ ]
368
+ )
369
+
370
+ return APIResponse(
371
+ meta=Meta(
372
+ path=request.url.path,
373
+ path_template="objects",
374
+ params={
375
+ "database_name": database_name,
376
+ "collection_name": collection_name,
377
+ },
378
+ paged=True,
379
+ item_count=total_count,
380
+ page=current_page,
381
+ page_size=limit,
382
+ ),
383
+ item_type=ItemType(
384
+ name=cd.name,
385
+ description=cd.description,
386
+ ),
387
+ items=items,
388
+ data={},
389
+ links=links,
390
+ )
391
+
392
+
393
+ @app.get("/databases/{database_name}/collections/{collection_name}/objects/{id}", response_model=APIResponse)
394
+ async def get_object_details(
395
+ request: Request,
396
+ database_name: str,
397
+ collection_name: str,
398
+ id: str,
399
+ get_db: Callable[[str], Database] = Depends(get_database),
400
+ ):
401
+ database = get_db(database_name)
402
+ collection = database.get_collection(collection_name)
403
+ ids = id.split("+")
404
+ result = collection.get(ids)
405
+
406
+ base_url = f"/databases/{database_name}/collections/{collection_name}/objects/{id}"
407
+ links = [Link(rel="self", href=base_url)]
408
+ links.extend(
409
+ [
410
+ Link(rel="collection", href=f"/databases/{database_name}/collections/{collection_name}"),
411
+ Link(rel="database", href=f"/databases/{database_name}"),
412
+ ]
413
+ )
414
+
415
+ return APIResponse(
416
+ meta=Meta(
417
+ path=request.url.path,
418
+ path_template="objects",
419
+ params={
420
+ "database_name": database_name,
421
+ "collection_name": collection_name,
422
+ },
423
+ ),
424
+ data={
425
+ "domain_objects": result.rows,
426
+ },
427
+ links=links,
428
+ )
429
+
430
+
431
+ @app.get("/databases/{database_name}/collections/{collection_name}/search/{term}", response_model=APIResponse)
432
+ async def search_objects(
433
+ request: Request,
434
+ database_name: str,
435
+ collection_name: str,
436
+ term: str,
437
+ limit: int = Query(5, ge=1, le=100),
438
+ offset: int = Query(0, ge=0),
439
+ get_db: Callable[[str], Database] = Depends(get_database),
440
+ ):
441
+ database = get_db(database_name)
442
+ collection = database.get_collection(collection_name)
443
+ result = collection.search(term, limit=limit)
444
+
445
+ total_count = result.num_rows
446
+ total_pages = (total_count + limit - 1) // limit
447
+ current_page = offset // limit + 1
448
+
449
+ base_url = f"/databases/{database_name}/collections/{collection_name}/search/{term}"
450
+ links = [Link(rel="self", href=f"{base_url}?limit={limit}&offset={offset}")]
451
+ if current_page > 1:
452
+ links.append(Link(rel="prev", href=f"{base_url}?limit={limit}&offset={offset - limit}"))
453
+ if current_page < total_pages:
454
+ links.append(Link(rel="next", href=f"{base_url}?limit={limit}&offset={offset + limit}"))
455
+
456
+ links.extend(
457
+ [
458
+ Link(rel="first", href=f"{base_url}?limit={limit}&offset=0"),
459
+ Link(rel="last", href=f"{base_url}?limit={limit}&offset={(total_pages - 1) * limit}"),
460
+ Link(rel="collection", href=f"/databases/{database_name}/collections/{collection_name}"),
461
+ Link(rel="database", href=f"/databases/{database_name}"),
462
+ ]
463
+ )
464
+
465
+ return APIResponse(
466
+ meta=Meta(
467
+ path=request.url.path,
468
+ path_template="collection_search",
469
+ params={
470
+ "database_name": database_name,
471
+ "collection_name": collection_name,
472
+ "term": term,
473
+ },
474
+ ),
475
+ data={
476
+ "objects": result.ranked_rows,
477
+ "total_count": total_count,
478
+ "page": current_page,
479
+ "total_pages": total_pages,
480
+ "page_size": limit,
481
+ },
482
+ links=links,
483
+ )
484
+
485
+
486
+ @app.get("/databases/{database_name}/collections/{collection_name}/facets", response_model=APIResponse)
487
+ async def list_collection_facets(
488
+ request: Request,
489
+ database_name: str,
490
+ collection_name: str,
491
+ where: Optional[str] = None,
492
+ limit: int = Query(10, ge=1, le=100),
493
+ offset: int = Query(0, ge=0),
494
+ get_db: Callable[[str], Database] = Depends(get_database),
495
+ ):
496
+ # DEPRECATED?
497
+ database = get_db(database_name)
498
+ collection = database.get_collection(collection_name)
499
+ where_clause = load_objects(where) if where else None
500
+ results = collection.query_facets(where_clause)
501
+
502
+ total_count = collection.find({}).num_rows
503
+ total_pages = (total_count + limit - 1) // limit
504
+ current_page = offset // limit + 1
505
+
506
+ base_url = f"/databases/{database_name}/collections/{collection_name}/facets"
507
+ links = [Link(rel="self", href=f"{base_url}?limit={limit}&offset={offset}")]
508
+ if current_page > 1:
509
+ links.append(Link(rel="prev", href=f"{base_url}?limit={limit}&offset={offset - limit}"))
510
+ if current_page < total_pages:
511
+ links.append(Link(rel="next", href=f"{base_url}?limit={limit}&offset={offset + limit}"))
512
+
513
+ links.extend(
514
+ [
515
+ Link(rel="first", href=f"{base_url}?limit={limit}&offset=0"),
516
+ Link(rel="last", href=f"{base_url}?limit={limit}&offset={(total_pages - 1) * limit}"),
517
+ Link(rel="collection", href=f"/databases/{database_name}/collections/{collection_name}"),
518
+ Link(rel="database", href=f"/databases/{database_name}"),
519
+ ]
520
+ )
521
+
522
+ return APIResponse(
523
+ meta=Meta(
524
+ path=request.url.path,
525
+ path_template="facets",
526
+ params={
527
+ "database_name": database_name,
528
+ "collection_name": collection_name,
529
+ },
530
+ ),
531
+ data={
532
+ "items": results,
533
+ "total_count": total_count,
534
+ "page": current_page,
535
+ "total_pages": total_pages,
536
+ "page_size": limit,
537
+ },
538
+ links=links,
539
+ )
540
+
541
+
542
+ @app.get("/databases/{database_name}/collections/{collection_name}/attributes", response_model=APIResponse)
543
+ async def list_collection_attributes(
544
+ request: Request,
545
+ database_name: str,
546
+ collection_name: str,
547
+ where: Optional[str] = None,
548
+ get_db: Callable[[str], Database] = Depends(get_database),
549
+ ):
550
+ database = get_db(database_name)
551
+ collection = database.get_collection(collection_name)
552
+ where_clause = load_objects(where) if where else None
553
+ base_url = f"/databases/{database_name}/collections/{collection_name}/attributes"
554
+ results = collection.query_facets(where_clause)
555
+ items = [
556
+ Item(
557
+ name=facet_att,
558
+ type="Attribute",
559
+ links=[
560
+ Link(rel="self", href=f"{base_url}/{facet_att}"),
561
+ ],
562
+ data=[{"value": v, "count": c} for v, c in data],
563
+ )
564
+ for facet_att, data in results.items()
565
+ ]
566
+
567
+ links = [
568
+ Link(rel="self", href=base_url),
569
+ Link(rel="parent", href=f"/databases/{database_name}/collections/{collection_name}"),
570
+ Link(rel="grandparent", href=f"/databases/{database_name}"),
571
+ ]
572
+
573
+ return APIResponse(
574
+ meta=Meta(
575
+ path=request.url.path,
576
+ path_template="facets",
577
+ params={
578
+ "database_name": database_name,
579
+ "collection_name": collection_name,
580
+ },
581
+ ),
582
+ data={},
583
+ items=items,
584
+ links=links,
585
+ )
586
+
587
+
588
+ @app.get(
589
+ "/databases/{database_name}/collections/{collection_name}/attributes/{attribute_name}", response_model=APIResponse
590
+ )
591
+ async def get_attribute_details(
592
+ request: Request,
593
+ database_name: str,
594
+ collection_name: str,
595
+ attribute_name: str,
596
+ where: Optional[str] = None,
597
+ get_db: Callable[[str], Database] = Depends(get_database),
598
+ ):
599
+ database = get_db(database_name)
600
+ collection = database.get_collection(collection_name)
601
+ where_clause = load_objects(where) if where else None
602
+ base_url = f"/databases/{database_name}/collections/{collection_name}/attributes/{attribute_name}"
603
+ count_tuples = collection.query_facets(where_clause, facet_columns=[attribute_name])[attribute_name]
604
+ _count_objs = [{"value": v, "count": c} for v, c in count_tuples]
605
+ cd = collection.class_definition()
606
+ att = cd.attributes[attribute_name]
607
+ att_dict = json_dumper.to_dict(att)
608
+ items = [
609
+ Item(
610
+ name=str(v),
611
+ type="Value",
612
+ links=[
613
+ Link(rel="self", href=f"{base_url}/equals/{v}"),
614
+ ],
615
+ data={"count": c},
616
+ )
617
+ for v, c in count_tuples
618
+ ]
619
+
620
+ links = [
621
+ Link(rel="self", href=base_url),
622
+ Link(rel="collection", href=f"/databases/{database_name}/collections/{collection_name}"),
623
+ Link(rel="database", href=f"/databases/{database_name}"),
624
+ ]
625
+
626
+ return APIResponse(
627
+ meta=Meta(
628
+ path=request.url.path,
629
+ path_template="facets",
630
+ params={
631
+ "database_name": database_name,
632
+ "collection_name": collection_name,
633
+ },
634
+ ),
635
+ data={
636
+ "attribute": att_dict,
637
+ },
638
+ items=items,
639
+ links=links,
640
+ )
641
+
642
+
643
+ @app.get(
644
+ "/databases/{database_name}/collections/{collection_name}/attributes/{attribute_name}/equals/{value}",
645
+ response_model=APIResponse,
646
+ )
647
+ async def query_by_attribute(
648
+ request: Request,
649
+ database_name: str,
650
+ collection_name: str,
651
+ attribute_name: str,
652
+ value: str,
653
+ where: Optional[str] = None,
654
+ limit: int = Query(10, ge=1, le=100),
655
+ offset: int = Query(0, ge=0),
656
+ get_db: Callable[[str], Database] = Depends(get_database),
657
+ ):
658
+ database = get_db(database_name)
659
+ collection = database.get_collection(collection_name)
660
+ where_clause = {attribute_name: value}
661
+ query = StoreQuery(from_table=collection.alias, where_clause=where_clause, limit=limit, offset=offset)
662
+ result = collection.query(query)
663
+ items = []
664
+ for i, row in enumerate(result.rows):
665
+ item = Item(name=str(i), type="X", data=row, links=[])
666
+ items.append(item)
667
+
668
+ total_count = collection.find({}).num_rows
669
+ total_pages = (total_count + limit - 1) // limit
670
+ current_page = offset // limit + 1
671
+
672
+ base_url = f"/databases/{database_name}/collections/{collection_name}/attributes/{attribute_name}/equals/{value}"
673
+ links = [Link(rel="self", href=f"{base_url}?limit={limit}&offset={offset}")]
674
+ if current_page > 1:
675
+ links.append(Link(rel="prev", href=f"{base_url}?limit={limit}&offset={offset - limit}"))
676
+ if current_page < total_pages:
677
+ links.append(Link(rel="next", href=f"{base_url}?limit={limit}&offset={offset + limit}"))
678
+
679
+ links.extend(
680
+ [
681
+ Link(rel="first", href=f"{base_url}?limit={limit}&offset=0"),
682
+ Link(rel="last", href=f"{base_url}?limit={limit}&offset={(total_pages - 1) * limit}"),
683
+ Link(
684
+ rel="parent",
685
+ href=f"/databases/{database_name}/collections/{collection_name}/attributes/{attribute_name}",
686
+ ),
687
+ ]
688
+ )
689
+
690
+ return APIResponse(
691
+ meta=Meta(
692
+ path=request.url.path,
693
+ path_template="objects",
694
+ params={
695
+ "database_name": database_name,
696
+ "collection_name": collection_name,
697
+ },
698
+ paged=True,
699
+ item_count=total_count,
700
+ page=current_page,
701
+ page_size=limit,
702
+ ),
703
+ items=items,
704
+ data={},
705
+ links=links,
706
+ )
707
+
708
+
709
+ @app.post("/databases/{database_name}/collections/{collection_name}/objects", response_model=APIResponse)
710
+ async def insert_objects(
711
+ database_name: str,
712
+ collection_name: str,
713
+ insert: ObjectInsert,
714
+ get_db: Callable[[str], Database] = Depends(get_database),
715
+ ):
716
+ database = get_db(database_name)
717
+ collection = database.get_collection(collection_name)
718
+ collection.insert(insert.objects)
719
+ return APIResponse(
720
+ data={"inserted_count": len(insert.objects)},
721
+ links=[
722
+ Link(rel="self", href=f"/databases/{database_name}/collections/{collection_name}/objects"),
723
+ Link(rel="collection", href=f"/databases/{database_name}/collections/{collection_name}"),
724
+ ],
725
+ )
726
+
727
+
728
+ @app.get("/databases/{database_name}/schema", response_model=APIResponse)
729
+ async def get_database_schema(
730
+ request: Request, database_name: str, get_db: Callable[[str], Database] = Depends(get_database)
731
+ ):
732
+ database = get_db(database_name)
733
+ schema = database.schema_view.schema
734
+ schema_dict = json_dumper.to_dict(schema)
735
+ return APIResponse(
736
+ meta=Meta(path=request.url.path, path_template="schema", params={"database_name": database_name}),
737
+ data={"schema": schema_dict},
738
+ links=[
739
+ Link(rel="self", href=f"/databases/{database_name}/schema"),
740
+ Link(rel="database", href=f"/databases/{database_name}"),
741
+ ],
742
+ )
743
+
744
+
745
+ @app.get("/xxxpages/{path:path}", response_class=HTMLResponse)
746
+ async def xxxgeneric_page(request: Request, path: str, get_db: Callable[[str], Database] = Depends(get_database)):
747
+ # Construct the API URL
748
+ api_url = f"{request.base_url}{path}"
749
+ query_params = dict(request.query_params)
750
+
751
+ # Make a request to the API
752
+ async with httpx.AsyncClient() as client:
753
+ response = await client.get(api_url, params=query_params)
754
+
755
+ if response.status_code != 200:
756
+ raise HTTPException(status_code=response.status_code, detail=response.text)
757
+
758
+ # Parse the JSON response
759
+ api_data = response.json()
760
+ # api_response = APIResponse(**api_data)
761
+
762
+ # Use the template specified in the API response
763
+ meta = api_data["meta"]
764
+ # template_name = meta['path_template'] + ".html.j2"
765
+ template_name = "generic.html.j2"
766
+ params = meta["params"]
767
+
768
+ data = api_data["data"]
769
+ data_html = None
770
+ if "domain_objects" in data:
771
+ objs = data["domain_objects"]
772
+ db = get_db(params["database_name"])
773
+ sv = db.schema_view
774
+ collection = db.get_collection(params["collection_name"])
775
+ if collection.class_definition():
776
+ cn = collection.class_definition().name
777
+ style_engine = StyleEngine(schemaview=sv)
778
+ html_renderer.style_engine = style_engine
779
+ data_html = [html_renderer.render(obj, schemaview=sv, source_element_name=cn) for obj in objs]
780
+
781
+ # Render the appropriate template
782
+ return templates.TemplateResponse(
783
+ template_name,
784
+ {
785
+ "request": request,
786
+ "response": api_data,
787
+ "current_path": f"/pages/{path}",
788
+ "data_html": data_html,
789
+ "params": params,
790
+ },
791
+ )
792
+
793
+
794
+ @app.get("/pages/{path:path}", response_class=HTMLResponse)
795
+ async def generic_page(request: Request, path: str, get_db: Callable[[str], Database] = Depends(get_database)):
796
+ # Construct the API URL
797
+ api_url = f"{request.base_url}{path}"
798
+ query_params = dict(request.query_params)
799
+
800
+ # Make a request to the API
801
+ async with httpx.AsyncClient() as client:
802
+ response = await client.get(api_url, params=query_params)
803
+
804
+ if response.status_code != 200:
805
+ raise HTTPException(status_code=response.status_code, detail=response.text)
806
+
807
+ # Parse the JSON response
808
+ api_data = response.json()
809
+ payload = APIResponse(**api_data)
810
+ template_name = "generic.html.j2"
811
+ params = payload.meta.params
812
+
813
+ # data = payload.data
814
+ data_html = None
815
+ if "database_name" in params:
816
+ db = get_db(params["database_name"])
817
+ sv = db.schema_view
818
+ else:
819
+ sv = None
820
+ if not payload.items:
821
+ payload.items = []
822
+ if payload.item_type and payload.items:
823
+ cn = payload.item_type.name
824
+ style_engine = StyleEngine(schemaview=sv)
825
+ html_renderer.style_engine = style_engine
826
+ for item in payload.items:
827
+ if item.data:
828
+ item.html = html_renderer.render(item.data, schemaview=sv, source_element_name=cn)
829
+
830
+ # Render the appropriate template
831
+ return templates.TemplateResponse(
832
+ template_name,
833
+ {
834
+ "request": request,
835
+ "response": payload,
836
+ "current_path": f"/pages/{path}",
837
+ "data_html": data_html,
838
+ "params": params,
839
+ },
840
+ )
841
+
842
+
843
+ def run_server():
844
+ import uvicorn
845
+
846
+ uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
847
+
848
+
849
+ # if __name__ == "__main__":
850
+ # run_server()
851
+
852
+
853
+ def start():
854
+ """Launched with `poetry run start` at root level"""
855
+ uvicorn.run("linkml_store.webapi.main:app", host="127.0.0.1", port=8000, reload=True)