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.
- linkml_store/api/client.py +15 -4
- linkml_store/api/collection.py +185 -15
- linkml_store/api/config.py +11 -3
- linkml_store/api/database.py +36 -5
- linkml_store/api/stores/duckdb/duckdb_collection.py +6 -3
- linkml_store/api/stores/duckdb/duckdb_database.py +20 -1
- linkml_store/api/stores/filesystem/__init__.py +7 -8
- linkml_store/api/stores/filesystem/filesystem_collection.py +150 -113
- linkml_store/api/stores/filesystem/filesystem_database.py +57 -21
- linkml_store/api/stores/mongodb/mongodb_collection.py +82 -34
- linkml_store/api/stores/mongodb/mongodb_database.py +13 -2
- linkml_store/api/types.py +4 -0
- linkml_store/cli.py +97 -8
- linkml_store/index/__init__.py +5 -3
- linkml_store/index/indexer.py +7 -2
- linkml_store/utils/change_utils.py +17 -0
- linkml_store/utils/format_utils.py +89 -8
- linkml_store/utils/patch_utils.py +126 -0
- linkml_store/utils/query_utils.py +89 -0
- linkml_store/utils/schema_utils.py +23 -0
- linkml_store/webapi/__init__.py +0 -0
- linkml_store/webapi/html/__init__.py +3 -0
- linkml_store/webapi/html/base.html.j2 +24 -0
- linkml_store/webapi/html/collection_details.html.j2 +15 -0
- linkml_store/webapi/html/database_details.html.j2 +16 -0
- linkml_store/webapi/html/databases.html.j2 +14 -0
- linkml_store/webapi/html/generic.html.j2 +46 -0
- linkml_store/webapi/main.py +572 -0
- linkml_store-0.1.10.dist-info/METADATA +138 -0
- linkml_store-0.1.10.dist-info/RECORD +58 -0
- {linkml_store-0.1.8.dist-info → linkml_store-0.1.10.dist-info}/entry_points.txt +1 -0
- linkml_store-0.1.8.dist-info/METADATA +0 -58
- linkml_store-0.1.8.dist-info/RECORD +0 -45
- {linkml_store-0.1.8.dist-info → linkml_store-0.1.10.dist-info}/LICENSE +0 -0
- {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)
|