morphik 1.2.0__tar.gz → 1.2.2__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.
- {morphik-1.2.0 → morphik-1.2.2}/.gitignore +2 -1
- {morphik-1.2.0 → morphik-1.2.2}/PKG-INFO +11 -1
- {morphik-1.2.0 → morphik-1.2.2}/README.md +10 -0
- {morphik-1.2.0 → morphik-1.2.2}/morphik/__init__.py +1 -1
- {morphik-1.2.0 → morphik-1.2.2}/morphik/_internal.py +22 -3
- {morphik-1.2.0 → morphik-1.2.2}/morphik/_shared.py +41 -24
- {morphik-1.2.0 → morphik-1.2.2}/morphik/async_.py +80 -36
- {morphik-1.2.0 → morphik-1.2.2}/morphik/sync.py +80 -36
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_app_ops.py +4 -5
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_async.py +76 -2
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_scoped_ops_unit.py +462 -0
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_shared_helpers.py +3 -17
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_sync.py +69 -2
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_update_document_metadata_rename.py +0 -1
- {morphik-1.2.0 → morphik-1.2.2}/pyproject.toml +1 -1
- {morphik-1.2.0 → morphik-1.2.2}/morphik/_scoped_ops.py +0 -0
- {morphik-1.2.0 → morphik-1.2.2}/morphik/exceptions.py +0 -0
- {morphik-1.2.0 → morphik-1.2.2}/morphik/models.py +0 -0
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/README.md +0 -0
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/__init__.py +0 -0
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/example_usage.py +0 -0
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_docs/sample1.txt +0 -0
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_docs/sample2.txt +0 -0
- {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_docs/sample3.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: morphik
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Morphik Python Client
|
|
5
5
|
Author-email: Morphik <founders@morphik.ai>
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -52,6 +52,9 @@ from morphik import Morphik
|
|
|
52
52
|
# Initialize client - connects to localhost:8000 by default
|
|
53
53
|
db = Morphik()
|
|
54
54
|
|
|
55
|
+
# You can also use a direct HTTP(S) base URL for self-hosted deployments
|
|
56
|
+
# db = Morphik("http://morphik:8000")
|
|
57
|
+
|
|
55
58
|
# Or with authentication URI (for production)
|
|
56
59
|
# db = Morphik("morphik://owner_id:token@api.morphik.ai")
|
|
57
60
|
|
|
@@ -96,6 +99,10 @@ print(response.completion)
|
|
|
96
99
|
# Create a nested folder (parents are auto-created server-side)
|
|
97
100
|
folder = db.create_folder(full_path="/projects/alpha/specs", description="Specs folder")
|
|
98
101
|
|
|
102
|
+
# Move or rename folder paths
|
|
103
|
+
moved = db.move_folder("/projects/alpha/specs", "/projects/archive/specs")
|
|
104
|
+
renamed = moved.rename("specs-v2")
|
|
105
|
+
|
|
99
106
|
# Scope queries to a path and include descendants with folder_depth=-1
|
|
100
107
|
chunks = folder.retrieve_chunks(query="design notes", folder_depth=-1)
|
|
101
108
|
docs = db.list_documents(folder_name="/projects/alpha", folder_depth=-1)
|
|
@@ -113,6 +120,9 @@ async def main():
|
|
|
113
120
|
# Initialize async client - connects to localhost:8000 by default
|
|
114
121
|
async with AsyncMorphik() as db:
|
|
115
122
|
|
|
123
|
+
# You can also use a direct HTTP(S) base URL for self-hosted deployments
|
|
124
|
+
# async with AsyncMorphik("http://morphik:8000") as db:
|
|
125
|
+
|
|
116
126
|
# Or with authentication URI (for production)
|
|
117
127
|
# async with AsyncMorphik("morphik://owner_id:token@api.morphik.ai") as db:
|
|
118
128
|
# Ingest a text document
|
|
@@ -39,6 +39,9 @@ from morphik import Morphik
|
|
|
39
39
|
# Initialize client - connects to localhost:8000 by default
|
|
40
40
|
db = Morphik()
|
|
41
41
|
|
|
42
|
+
# You can also use a direct HTTP(S) base URL for self-hosted deployments
|
|
43
|
+
# db = Morphik("http://morphik:8000")
|
|
44
|
+
|
|
42
45
|
# Or with authentication URI (for production)
|
|
43
46
|
# db = Morphik("morphik://owner_id:token@api.morphik.ai")
|
|
44
47
|
|
|
@@ -83,6 +86,10 @@ print(response.completion)
|
|
|
83
86
|
# Create a nested folder (parents are auto-created server-side)
|
|
84
87
|
folder = db.create_folder(full_path="/projects/alpha/specs", description="Specs folder")
|
|
85
88
|
|
|
89
|
+
# Move or rename folder paths
|
|
90
|
+
moved = db.move_folder("/projects/alpha/specs", "/projects/archive/specs")
|
|
91
|
+
renamed = moved.rename("specs-v2")
|
|
92
|
+
|
|
86
93
|
# Scope queries to a path and include descendants with folder_depth=-1
|
|
87
94
|
chunks = folder.retrieve_chunks(query="design notes", folder_depth=-1)
|
|
88
95
|
docs = db.list_documents(folder_name="/projects/alpha", folder_depth=-1)
|
|
@@ -100,6 +107,9 @@ async def main():
|
|
|
100
107
|
# Initialize async client - connects to localhost:8000 by default
|
|
101
108
|
async with AsyncMorphik() as db:
|
|
102
109
|
|
|
110
|
+
# You can also use a direct HTTP(S) base URL for self-hosted deployments
|
|
111
|
+
# async with AsyncMorphik("http://morphik:8000") as db:
|
|
112
|
+
|
|
103
113
|
# Or with authentication URI (for production)
|
|
104
114
|
# async with AsyncMorphik("morphik://owner_id:token@api.morphik.ai") as db:
|
|
105
115
|
# Ingest a text document
|
|
@@ -64,12 +64,31 @@ class _MorphikClientLogic:
|
|
|
64
64
|
if not parsed.netloc:
|
|
65
65
|
raise ValueError("Invalid URI format")
|
|
66
66
|
|
|
67
|
+
# Allow direct HTTP(S) endpoints without auth token for self-hosted usage.
|
|
68
|
+
if parsed.scheme in {"http", "https"}:
|
|
69
|
+
path_prefix = (parsed.path or "").rstrip("/")
|
|
70
|
+
self._base_url = f"{parsed.scheme}://{parsed.netloc}{path_prefix}".rstrip("/")
|
|
71
|
+
self._auth_token = None
|
|
72
|
+
if parsed.hostname in {"localhost", "127.0.0.1", "0.0.0.0", "host.docker.internal"}:
|
|
73
|
+
self._is_local = True
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
if parsed.scheme != "morphik":
|
|
77
|
+
raise ValueError("Invalid URI format")
|
|
78
|
+
if "@" not in parsed.netloc:
|
|
79
|
+
raise ValueError("Invalid Morphik URI format. Expected morphik://<owner_id>:<token>@<host>")
|
|
80
|
+
|
|
67
81
|
# Split host and auth parts
|
|
68
|
-
auth, host = parsed.netloc.
|
|
69
|
-
|
|
82
|
+
auth, host = parsed.netloc.rsplit("@", 1)
|
|
83
|
+
if ":" not in auth:
|
|
84
|
+
raise ValueError("Invalid Morphik URI auth format. Expected <owner_id>:<token>")
|
|
85
|
+
_, self._auth_token = auth.split(":", 1)
|
|
86
|
+
|
|
87
|
+
if parsed.hostname in {"localhost", "127.0.0.1", "0.0.0.0", "host.docker.internal"}:
|
|
88
|
+
self._is_local = True
|
|
70
89
|
|
|
71
90
|
# Set base URL
|
|
72
|
-
self._base_url = f"{'http' if self._is_local else 'https'}://{host}"
|
|
91
|
+
self._base_url = f"{'http' if self._is_local else 'https'}://{host}".rstrip("/")
|
|
73
92
|
|
|
74
93
|
# Basic token validation
|
|
75
94
|
jwt.decode(self._auth_token, options={"verify_signature": False})
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
6
|
+
from urllib.parse import quote
|
|
6
7
|
|
|
7
8
|
from pydantic import BaseModel
|
|
8
9
|
|
|
@@ -95,27 +96,8 @@ def build_rotate_app_params(
|
|
|
95
96
|
return params
|
|
96
97
|
|
|
97
98
|
|
|
98
|
-
def build_create_app_payload(
|
|
99
|
-
|
|
100
|
-
name: str,
|
|
101
|
-
app_id: Optional[str],
|
|
102
|
-
user_id: Optional[str],
|
|
103
|
-
expiry_days: Optional[int],
|
|
104
|
-
org_id: Optional[str],
|
|
105
|
-
created_by_user_id: Optional[str],
|
|
106
|
-
) -> Dict[str, Any]:
|
|
107
|
-
payload: Dict[str, Any] = {"name": name}
|
|
108
|
-
if app_id is not None:
|
|
109
|
-
payload["app_id"] = app_id
|
|
110
|
-
if user_id is not None:
|
|
111
|
-
payload["user_id"] = user_id
|
|
112
|
-
if expiry_days is not None:
|
|
113
|
-
payload["expiry_days"] = expiry_days
|
|
114
|
-
if org_id is not None:
|
|
115
|
-
payload["org_id"] = org_id
|
|
116
|
-
if created_by_user_id is not None:
|
|
117
|
-
payload["created_by_user_id"] = created_by_user_id
|
|
118
|
-
return payload
|
|
99
|
+
def build_create_app_payload(*, name: str) -> Dict[str, Any]:
|
|
100
|
+
return {"name": name}
|
|
119
101
|
|
|
120
102
|
|
|
121
103
|
def build_requeue_payload(
|
|
@@ -140,9 +122,7 @@ def build_requeue_payload(
|
|
|
140
122
|
return payload
|
|
141
123
|
|
|
142
124
|
|
|
143
|
-
def build_logs_params(
|
|
144
|
-
*, limit: int, hours: float, op_type: Optional[str], status: Optional[str]
|
|
145
|
-
) -> Dict[str, Any]:
|
|
125
|
+
def build_logs_params(*, limit: int, hours: float, op_type: Optional[str], status: Optional[str]) -> Dict[str, Any]:
|
|
146
126
|
params: Dict[str, Any] = {
|
|
147
127
|
"limit": max(1, min(limit, MAX_LIMIT)),
|
|
148
128
|
"hours": max(MIN_LOG_HOURS, min(hours, MAX_LOG_HOURS)),
|
|
@@ -167,6 +147,43 @@ def build_document_by_filename_params(
|
|
|
167
147
|
return params
|
|
168
148
|
|
|
169
149
|
|
|
150
|
+
def normalize_folder_identifier(folder_id_or_name: str) -> str:
|
|
151
|
+
if not folder_id_or_name:
|
|
152
|
+
raise ValueError("folder_id_or_name is required")
|
|
153
|
+
normalized = folder_id_or_name.lstrip("/")
|
|
154
|
+
if not normalized:
|
|
155
|
+
raise ValueError("folder_id_or_name is required")
|
|
156
|
+
return normalized
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def build_folder_endpoint_identifier(folder_id_or_name: str) -> str:
|
|
160
|
+
normalized = normalize_folder_identifier(folder_id_or_name)
|
|
161
|
+
return quote(normalized, safe="/")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def build_folder_move_payload(*, new_path: str) -> Dict[str, str]:
|
|
165
|
+
normalized_path = "/" + (new_path or "").strip("/")
|
|
166
|
+
if normalized_path == "/":
|
|
167
|
+
raise ValueError("new_path must include at least one non-root segment")
|
|
168
|
+
return {"new_path": normalized_path}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def build_folder_rename_path(*, current_path: str, new_name: str) -> str:
|
|
172
|
+
normalized_name = (new_name or "").strip().strip("/")
|
|
173
|
+
if not normalized_name:
|
|
174
|
+
raise ValueError("new_name is required")
|
|
175
|
+
if "/" in normalized_name:
|
|
176
|
+
raise ValueError("new_name must be a single folder segment without '/'")
|
|
177
|
+
|
|
178
|
+
normalized_current = "/" + (current_path or "").strip("/")
|
|
179
|
+
segments = [segment for segment in normalized_current.split("/") if segment]
|
|
180
|
+
if not segments:
|
|
181
|
+
raise ValueError("current_path must include at least one non-root segment")
|
|
182
|
+
|
|
183
|
+
segments[-1] = normalized_name
|
|
184
|
+
return "/" + "/".join(segments)
|
|
185
|
+
|
|
186
|
+
|
|
170
187
|
def normalize_additional_folders(
|
|
171
188
|
additional_folders: Optional[List[str]],
|
|
172
189
|
folder_name: Optional[Union[str, List[str]]],
|
|
@@ -11,19 +11,22 @@ from pydantic import BaseModel
|
|
|
11
11
|
|
|
12
12
|
from ._internal import FinalChunkResult, _MorphikClientLogic
|
|
13
13
|
from ._scoped_ops import _ScopedOperationsMixin
|
|
14
|
-
from .models import CompletionResponse # Prompt override models
|
|
15
14
|
from ._shared import (
|
|
15
|
+
build_folder_endpoint_identifier,
|
|
16
|
+
build_folder_move_payload,
|
|
17
|
+
build_folder_rename_path,
|
|
16
18
|
build_create_app_payload,
|
|
17
19
|
build_document_by_filename_params,
|
|
18
20
|
build_list_apps_params,
|
|
19
21
|
build_logs_params,
|
|
20
|
-
build_requeue_payload,
|
|
21
22
|
build_rename_app_params,
|
|
23
|
+
build_requeue_payload,
|
|
22
24
|
build_rotate_app_params,
|
|
23
25
|
collect_directory_files,
|
|
24
26
|
merge_folders,
|
|
25
27
|
normalize_additional_folders,
|
|
26
28
|
)
|
|
29
|
+
from .models import CompletionResponse # Prompt override models
|
|
27
30
|
from .models import (
|
|
28
31
|
AppStorageUsageResponse,
|
|
29
32
|
ChunkSource,
|
|
@@ -128,9 +131,7 @@ class _AsyncScopedClientOps:
|
|
|
128
131
|
if not files:
|
|
129
132
|
return []
|
|
130
133
|
|
|
131
|
-
return await self.ingest_files(
|
|
132
|
-
files=files, metadata=metadata, use_colpali=use_colpali, parallel=parallel
|
|
133
|
-
)
|
|
134
|
+
return await self.ingest_files(files=files, metadata=metadata, use_colpali=use_colpali, parallel=parallel)
|
|
134
135
|
|
|
135
136
|
async def query_document(
|
|
136
137
|
self,
|
|
@@ -418,6 +419,15 @@ class AsyncFolder(_AsyncScopedClientOps):
|
|
|
418
419
|
"""Returns the folder ID if available."""
|
|
419
420
|
return self._id
|
|
420
421
|
|
|
422
|
+
def _sync_from_folder(self, folder: "AsyncFolder") -> None:
|
|
423
|
+
self._id = folder.id or self._id
|
|
424
|
+
self._name = folder.name
|
|
425
|
+
self._full_path = folder.full_path
|
|
426
|
+
self._parent_id = folder.parent_id
|
|
427
|
+
self._depth = folder.depth
|
|
428
|
+
self._child_count = folder.child_count
|
|
429
|
+
self._description = folder.description
|
|
430
|
+
|
|
421
431
|
async def get_info(self) -> Dict[str, Any]:
|
|
422
432
|
"""
|
|
423
433
|
Get detailed information about this folder.
|
|
@@ -448,6 +458,24 @@ class AsyncFolder(_AsyncScopedClientOps):
|
|
|
448
458
|
self._description = info.description or self._description
|
|
449
459
|
return info
|
|
450
460
|
|
|
461
|
+
async def move(self, new_path: str) -> "AsyncFolder":
|
|
462
|
+
"""Move this folder to a new canonical path and refresh local metadata."""
|
|
463
|
+
identifier = self._id or self.full_path
|
|
464
|
+
if not identifier:
|
|
465
|
+
raise ValueError("Folder identifier is missing")
|
|
466
|
+
moved = await self._client.move_folder(identifier, new_path)
|
|
467
|
+
self._sync_from_folder(moved)
|
|
468
|
+
return moved
|
|
469
|
+
|
|
470
|
+
async def rename(self, new_name: str) -> "AsyncFolder":
|
|
471
|
+
"""Rename this folder (leaf segment only) and refresh local metadata."""
|
|
472
|
+
identifier = self._id or self.full_path
|
|
473
|
+
if not identifier:
|
|
474
|
+
raise ValueError("Folder identifier is missing")
|
|
475
|
+
renamed = await self._client.rename_folder(identifier, new_name)
|
|
476
|
+
self._sync_from_folder(renamed)
|
|
477
|
+
return renamed
|
|
478
|
+
|
|
451
479
|
async def get_summary(self) -> Summary:
|
|
452
480
|
"""Retrieve the latest summary for this folder."""
|
|
453
481
|
identifier = self._id or self.full_path
|
|
@@ -492,7 +520,6 @@ class AsyncFolder(_AsyncScopedClientOps):
|
|
|
492
520
|
return None
|
|
493
521
|
|
|
494
522
|
|
|
495
|
-
|
|
496
523
|
class AsyncUserScope(_AsyncScopedClientOps):
|
|
497
524
|
"""
|
|
498
525
|
A user scope that allows operations to be scoped to a specific end user and optionally a folder.
|
|
@@ -525,7 +552,6 @@ class AsyncUserScope(_AsyncScopedClientOps):
|
|
|
525
552
|
return self._end_user_id
|
|
526
553
|
|
|
527
554
|
|
|
528
|
-
|
|
529
555
|
class AsyncMorphik(_ScopedOperationsMixin):
|
|
530
556
|
"""
|
|
531
557
|
Morphik client for document operations.
|
|
@@ -702,6 +728,50 @@ class AsyncMorphik(_ScopedOperationsMixin):
|
|
|
702
728
|
response = await self._request("DELETE", f"folders/{folder_id_or_name}")
|
|
703
729
|
return response
|
|
704
730
|
|
|
731
|
+
async def move_folder(self, folder_id_or_name: str, new_path: str) -> AsyncFolder:
|
|
732
|
+
"""
|
|
733
|
+
Move and/or rename a folder by setting its new canonical path.
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
folder_id_or_name: Folder ID, name, or full path
|
|
737
|
+
new_path: Target full path (e.g. "/projects/archive/specs")
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
AsyncFolder: Updated folder metadata
|
|
741
|
+
"""
|
|
742
|
+
folder_param = build_folder_endpoint_identifier(folder_id_or_name)
|
|
743
|
+
payload = build_folder_move_payload(new_path=new_path)
|
|
744
|
+
response = await self._request("POST", f"folders/{folder_param}/move", data=payload)
|
|
745
|
+
info = FolderInfo(**response)
|
|
746
|
+
return AsyncFolder(
|
|
747
|
+
self,
|
|
748
|
+
info.name,
|
|
749
|
+
folder_id=info.id,
|
|
750
|
+
full_path=info.full_path,
|
|
751
|
+
parent_id=info.parent_id,
|
|
752
|
+
depth=info.depth,
|
|
753
|
+
child_count=info.child_count,
|
|
754
|
+
description=info.description,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
async def rename_folder(self, folder_id_or_name: str, new_name: str) -> AsyncFolder:
|
|
758
|
+
"""
|
|
759
|
+
Rename a folder by changing only its leaf segment.
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
folder_id_or_name: Folder ID, name, or full path
|
|
763
|
+
new_name: New folder name (single segment, no '/')
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
AsyncFolder: Updated folder metadata
|
|
767
|
+
"""
|
|
768
|
+
folder_param = build_folder_endpoint_identifier(folder_id_or_name)
|
|
769
|
+
current_info = FolderInfo(**(await self._request("GET", f"folders/{folder_param}")))
|
|
770
|
+
current_path = current_info.full_path or current_info.name
|
|
771
|
+
new_path = build_folder_rename_path(current_path=current_path, new_name=new_name)
|
|
772
|
+
move_identifier = current_info.id or folder_param
|
|
773
|
+
return await self.move_folder(move_identifier, new_path)
|
|
774
|
+
|
|
705
775
|
async def get_folder_summary(self, folder_id_or_path: str) -> Summary:
|
|
706
776
|
"""Get the persisted summary for a folder."""
|
|
707
777
|
folder_param = folder_id_or_path.lstrip("/") if folder_id_or_path else folder_id_or_path
|
|
@@ -996,9 +1066,7 @@ class AsyncMorphik(_ScopedOperationsMixin):
|
|
|
996
1066
|
return []
|
|
997
1067
|
|
|
998
1068
|
# Use ingest_files with collected paths
|
|
999
|
-
return await self.ingest_files(
|
|
1000
|
-
files=files, metadata=metadata, use_colpali=use_colpali, parallel=parallel
|
|
1001
|
-
)
|
|
1069
|
+
return await self.ingest_files(files=files, metadata=metadata, use_colpali=use_colpali, parallel=parallel)
|
|
1002
1070
|
|
|
1003
1071
|
async def retrieve_chunks(
|
|
1004
1072
|
self,
|
|
@@ -1947,42 +2015,18 @@ class AsyncMorphik(_ScopedOperationsMixin):
|
|
|
1947
2015
|
self,
|
|
1948
2016
|
*,
|
|
1949
2017
|
name: str,
|
|
1950
|
-
app_id: Optional[str] = None,
|
|
1951
|
-
user_id: Optional[str] = None,
|
|
1952
|
-
expiry_days: Optional[int] = None,
|
|
1953
|
-
org_id: Optional[str] = None,
|
|
1954
|
-
created_by_user_id: Optional[str] = None,
|
|
1955
2018
|
) -> Dict[str, str]:
|
|
1956
2019
|
"""Create a cloud app and return its authenticated URI (async)."""
|
|
1957
|
-
payload = build_create_app_payload(
|
|
1958
|
-
name=name,
|
|
1959
|
-
app_id=app_id,
|
|
1960
|
-
user_id=user_id,
|
|
1961
|
-
expiry_days=expiry_days,
|
|
1962
|
-
org_id=org_id,
|
|
1963
|
-
created_by_user_id=created_by_user_id,
|
|
1964
|
-
)
|
|
2020
|
+
payload = build_create_app_payload(name=name)
|
|
1965
2021
|
return await self._request("POST", "cloud/generate_uri", data=payload)
|
|
1966
2022
|
|
|
1967
2023
|
async def generate_cloud_uri(
|
|
1968
2024
|
self,
|
|
1969
2025
|
*,
|
|
1970
2026
|
name: str,
|
|
1971
|
-
app_id: Optional[str] = None,
|
|
1972
|
-
user_id: Optional[str] = None,
|
|
1973
|
-
expiry_days: Optional[int] = None,
|
|
1974
|
-
org_id: Optional[str] = None,
|
|
1975
|
-
created_by_user_id: Optional[str] = None,
|
|
1976
2027
|
) -> Dict[str, str]:
|
|
1977
2028
|
"""Deprecated alias for create_app (async)."""
|
|
1978
|
-
return await self.create_app(
|
|
1979
|
-
name=name,
|
|
1980
|
-
app_id=app_id,
|
|
1981
|
-
user_id=user_id,
|
|
1982
|
-
expiry_days=expiry_days,
|
|
1983
|
-
org_id=org_id,
|
|
1984
|
-
created_by_user_id=created_by_user_id,
|
|
1985
|
-
)
|
|
2029
|
+
return await self.create_app(name=name)
|
|
1986
2030
|
|
|
1987
2031
|
async def requeue_ingestion_jobs(
|
|
1988
2032
|
self,
|
|
@@ -11,19 +11,22 @@ from pydantic import BaseModel
|
|
|
11
11
|
|
|
12
12
|
from ._internal import FinalChunkResult, _MorphikClientLogic
|
|
13
13
|
from ._scoped_ops import _ScopedOperationsMixin
|
|
14
|
-
from .models import CompletionResponse # Prompt override models
|
|
15
14
|
from ._shared import (
|
|
15
|
+
build_folder_endpoint_identifier,
|
|
16
|
+
build_folder_move_payload,
|
|
17
|
+
build_folder_rename_path,
|
|
16
18
|
build_create_app_payload,
|
|
17
19
|
build_document_by_filename_params,
|
|
18
20
|
build_list_apps_params,
|
|
19
21
|
build_logs_params,
|
|
20
|
-
build_requeue_payload,
|
|
21
22
|
build_rename_app_params,
|
|
23
|
+
build_requeue_payload,
|
|
22
24
|
build_rotate_app_params,
|
|
23
25
|
collect_directory_files,
|
|
24
26
|
merge_folders,
|
|
25
27
|
normalize_additional_folders,
|
|
26
28
|
)
|
|
29
|
+
from .models import CompletionResponse # Prompt override models
|
|
27
30
|
from .models import (
|
|
28
31
|
AppStorageUsageResponse,
|
|
29
32
|
ChunkSource,
|
|
@@ -136,9 +139,7 @@ class _ScopedClientOps:
|
|
|
136
139
|
if not files:
|
|
137
140
|
return []
|
|
138
141
|
|
|
139
|
-
return self.ingest_files(
|
|
140
|
-
files=files, metadata=metadata, use_colpali=use_colpali, parallel=parallel
|
|
141
|
-
)
|
|
142
|
+
return self.ingest_files(files=files, metadata=metadata, use_colpali=use_colpali, parallel=parallel)
|
|
142
143
|
|
|
143
144
|
def query_document(
|
|
144
145
|
self,
|
|
@@ -444,6 +445,15 @@ class Folder(_ScopedClientOps):
|
|
|
444
445
|
"""Returns the folder ID if available."""
|
|
445
446
|
return self._id
|
|
446
447
|
|
|
448
|
+
def _sync_from_folder(self, folder: "Folder") -> None:
|
|
449
|
+
self._id = folder.id or self._id
|
|
450
|
+
self._name = folder.name
|
|
451
|
+
self._full_path = folder.full_path
|
|
452
|
+
self._parent_id = folder.parent_id
|
|
453
|
+
self._depth = folder.depth
|
|
454
|
+
self._child_count = folder.child_count
|
|
455
|
+
self._description = folder.description
|
|
456
|
+
|
|
447
457
|
def get_info(self) -> Dict[str, Any]:
|
|
448
458
|
"""
|
|
449
459
|
Get detailed information about this folder.
|
|
@@ -475,6 +485,24 @@ class Folder(_ScopedClientOps):
|
|
|
475
485
|
self._description = info.description or self._description
|
|
476
486
|
return info
|
|
477
487
|
|
|
488
|
+
def move(self, new_path: str) -> "Folder":
|
|
489
|
+
"""Move this folder to a new canonical path and refresh local metadata."""
|
|
490
|
+
identifier = self._id or self.full_path
|
|
491
|
+
if not identifier:
|
|
492
|
+
raise ValueError("Folder identifier is missing")
|
|
493
|
+
moved = self._client.move_folder(identifier, new_path)
|
|
494
|
+
self._sync_from_folder(moved)
|
|
495
|
+
return moved
|
|
496
|
+
|
|
497
|
+
def rename(self, new_name: str) -> "Folder":
|
|
498
|
+
"""Rename this folder (leaf segment only) and refresh local metadata."""
|
|
499
|
+
identifier = self._id or self.full_path
|
|
500
|
+
if not identifier:
|
|
501
|
+
raise ValueError("Folder identifier is missing")
|
|
502
|
+
renamed = self._client.rename_folder(identifier, new_name)
|
|
503
|
+
self._sync_from_folder(renamed)
|
|
504
|
+
return renamed
|
|
505
|
+
|
|
478
506
|
def get_summary(self) -> Summary:
|
|
479
507
|
"""Retrieve the latest summary for this folder."""
|
|
480
508
|
identifier = self._id or self.full_path
|
|
@@ -519,7 +547,6 @@ class Folder(_ScopedClientOps):
|
|
|
519
547
|
return None
|
|
520
548
|
|
|
521
549
|
|
|
522
|
-
|
|
523
550
|
class UserScope(_ScopedClientOps):
|
|
524
551
|
"""
|
|
525
552
|
A user scope that allows operations to be scoped to a specific end user and optionally a folder.
|
|
@@ -552,7 +579,6 @@ class UserScope(_ScopedClientOps):
|
|
|
552
579
|
return self._end_user_id
|
|
553
580
|
|
|
554
581
|
|
|
555
|
-
|
|
556
582
|
class Morphik(_ScopedOperationsMixin):
|
|
557
583
|
"""
|
|
558
584
|
Morphik client for document operations.
|
|
@@ -733,6 +759,50 @@ class Morphik(_ScopedOperationsMixin):
|
|
|
733
759
|
response = self._request("DELETE", f"folders/{folder_id_or_name}")
|
|
734
760
|
return response
|
|
735
761
|
|
|
762
|
+
def move_folder(self, folder_id_or_name: str, new_path: str) -> Folder:
|
|
763
|
+
"""
|
|
764
|
+
Move and/or rename a folder by setting its new canonical path.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
folder_id_or_name: Folder ID, name, or full path
|
|
768
|
+
new_path: Target full path (e.g. "/projects/archive/specs")
|
|
769
|
+
|
|
770
|
+
Returns:
|
|
771
|
+
Folder: Updated folder metadata
|
|
772
|
+
"""
|
|
773
|
+
folder_param = build_folder_endpoint_identifier(folder_id_or_name)
|
|
774
|
+
payload = build_folder_move_payload(new_path=new_path)
|
|
775
|
+
response = self._request("POST", f"folders/{folder_param}/move", data=payload)
|
|
776
|
+
info = FolderInfo(**response)
|
|
777
|
+
return Folder(
|
|
778
|
+
self,
|
|
779
|
+
info.name,
|
|
780
|
+
folder_id=info.id,
|
|
781
|
+
full_path=info.full_path,
|
|
782
|
+
parent_id=info.parent_id,
|
|
783
|
+
depth=info.depth,
|
|
784
|
+
child_count=info.child_count,
|
|
785
|
+
description=info.description,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
def rename_folder(self, folder_id_or_name: str, new_name: str) -> Folder:
|
|
789
|
+
"""
|
|
790
|
+
Rename a folder by changing only its leaf segment.
|
|
791
|
+
|
|
792
|
+
Args:
|
|
793
|
+
folder_id_or_name: Folder ID, name, or full path
|
|
794
|
+
new_name: New folder name (single segment, no '/')
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
Folder: Updated folder metadata
|
|
798
|
+
"""
|
|
799
|
+
folder_param = build_folder_endpoint_identifier(folder_id_or_name)
|
|
800
|
+
current_info = FolderInfo(**self._request("GET", f"folders/{folder_param}"))
|
|
801
|
+
current_path = current_info.full_path or current_info.name
|
|
802
|
+
new_path = build_folder_rename_path(current_path=current_path, new_name=new_name)
|
|
803
|
+
move_identifier = current_info.id or folder_param
|
|
804
|
+
return self.move_folder(move_identifier, new_path)
|
|
805
|
+
|
|
736
806
|
def get_folder_summary(self, folder_id_or_path: str) -> Summary:
|
|
737
807
|
"""Get the persisted summary for a folder."""
|
|
738
808
|
folder_param = folder_id_or_path.lstrip("/") if folder_id_or_path else folder_id_or_path
|
|
@@ -1039,9 +1109,7 @@ class Morphik(_ScopedOperationsMixin):
|
|
|
1039
1109
|
return []
|
|
1040
1110
|
|
|
1041
1111
|
# Use ingest_files with collected paths
|
|
1042
|
-
return self.ingest_files(
|
|
1043
|
-
files=files, metadata=metadata, use_colpali=use_colpali, parallel=parallel
|
|
1044
|
-
)
|
|
1112
|
+
return self.ingest_files(files=files, metadata=metadata, use_colpali=use_colpali, parallel=parallel)
|
|
1045
1113
|
|
|
1046
1114
|
def retrieve_chunks(
|
|
1047
1115
|
self,
|
|
@@ -1984,42 +2052,18 @@ class Morphik(_ScopedOperationsMixin):
|
|
|
1984
2052
|
self,
|
|
1985
2053
|
*,
|
|
1986
2054
|
name: str,
|
|
1987
|
-
app_id: Optional[str] = None,
|
|
1988
|
-
user_id: Optional[str] = None,
|
|
1989
|
-
expiry_days: Optional[int] = None,
|
|
1990
|
-
org_id: Optional[str] = None,
|
|
1991
|
-
created_by_user_id: Optional[str] = None,
|
|
1992
2055
|
) -> Dict[str, str]:
|
|
1993
2056
|
"""Create a cloud app and return its authenticated URI."""
|
|
1994
|
-
payload = build_create_app_payload(
|
|
1995
|
-
name=name,
|
|
1996
|
-
app_id=app_id,
|
|
1997
|
-
user_id=user_id,
|
|
1998
|
-
expiry_days=expiry_days,
|
|
1999
|
-
org_id=org_id,
|
|
2000
|
-
created_by_user_id=created_by_user_id,
|
|
2001
|
-
)
|
|
2057
|
+
payload = build_create_app_payload(name=name)
|
|
2002
2058
|
return self._request("POST", "cloud/generate_uri", data=payload)
|
|
2003
2059
|
|
|
2004
2060
|
def generate_cloud_uri(
|
|
2005
2061
|
self,
|
|
2006
2062
|
*,
|
|
2007
2063
|
name: str,
|
|
2008
|
-
app_id: Optional[str] = None,
|
|
2009
|
-
user_id: Optional[str] = None,
|
|
2010
|
-
expiry_days: Optional[int] = None,
|
|
2011
|
-
org_id: Optional[str] = None,
|
|
2012
|
-
created_by_user_id: Optional[str] = None,
|
|
2013
2064
|
) -> Dict[str, str]:
|
|
2014
2065
|
"""Deprecated alias for create_app."""
|
|
2015
|
-
return self.create_app(
|
|
2016
|
-
name=name,
|
|
2017
|
-
app_id=app_id,
|
|
2018
|
-
user_id=user_id,
|
|
2019
|
-
expiry_days=expiry_days,
|
|
2020
|
-
org_id=org_id,
|
|
2021
|
-
created_by_user_id=created_by_user_id,
|
|
2022
|
-
)
|
|
2066
|
+
return self.create_app(name=name)
|
|
2023
2067
|
|
|
2024
2068
|
def requeue_ingestion_jobs(
|
|
2025
2069
|
self,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import pytest
|
|
2
|
-
|
|
3
2
|
from morphik.async_ import AsyncMorphik
|
|
4
3
|
from morphik.models import DetailedHealthCheckResponse, LogResponse, RequeueIngestionJob
|
|
5
4
|
from morphik.sync import Morphik
|
|
@@ -102,10 +101,10 @@ def test_sync_app_ops_payloads():
|
|
|
102
101
|
assert call["endpoint"] == "apps/rotate_token"
|
|
103
102
|
assert call["params"] == {"app_name": "demo", "expiry_days": 10}
|
|
104
103
|
|
|
105
|
-
client.create_app(name="demo"
|
|
104
|
+
client.create_app(name="demo")
|
|
106
105
|
call = calls.pop()
|
|
107
106
|
assert call["endpoint"] == "cloud/generate_uri"
|
|
108
|
-
assert call["data"] == {"name": "demo"
|
|
107
|
+
assert call["data"] == {"name": "demo"}
|
|
109
108
|
|
|
110
109
|
client.requeue_ingestion_jobs(jobs=[RequeueIngestionJob(external_id="doc-1")])
|
|
111
110
|
call = calls.pop()
|
|
@@ -146,10 +145,10 @@ async def test_async_app_ops_payloads():
|
|
|
146
145
|
assert call["endpoint"] == "apps/rotate_token"
|
|
147
146
|
assert call["params"] == {"app_id": "app-1"}
|
|
148
147
|
|
|
149
|
-
await client.create_app(name="demo"
|
|
148
|
+
await client.create_app(name="demo")
|
|
150
149
|
call = calls.pop()
|
|
151
150
|
assert call["endpoint"] == "cloud/generate_uri"
|
|
152
|
-
assert call["data"] == {"name": "demo"
|
|
151
|
+
assert call["data"] == {"name": "demo"}
|
|
153
152
|
|
|
154
153
|
await client.requeue_ingestion_jobs(include_all=True, statuses=["failed"])
|
|
155
154
|
call = calls.pop()
|