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.
Files changed (24) hide show
  1. {morphik-1.2.0 → morphik-1.2.2}/.gitignore +2 -1
  2. {morphik-1.2.0 → morphik-1.2.2}/PKG-INFO +11 -1
  3. {morphik-1.2.0 → morphik-1.2.2}/README.md +10 -0
  4. {morphik-1.2.0 → morphik-1.2.2}/morphik/__init__.py +1 -1
  5. {morphik-1.2.0 → morphik-1.2.2}/morphik/_internal.py +22 -3
  6. {morphik-1.2.0 → morphik-1.2.2}/morphik/_shared.py +41 -24
  7. {morphik-1.2.0 → morphik-1.2.2}/morphik/async_.py +80 -36
  8. {morphik-1.2.0 → morphik-1.2.2}/morphik/sync.py +80 -36
  9. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_app_ops.py +4 -5
  10. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_async.py +76 -2
  11. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_scoped_ops_unit.py +462 -0
  12. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_shared_helpers.py +3 -17
  13. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_sync.py +69 -2
  14. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_update_document_metadata_rename.py +0 -1
  15. {morphik-1.2.0 → morphik-1.2.2}/pyproject.toml +1 -1
  16. {morphik-1.2.0 → morphik-1.2.2}/morphik/_scoped_ops.py +0 -0
  17. {morphik-1.2.0 → morphik-1.2.2}/morphik/exceptions.py +0 -0
  18. {morphik-1.2.0 → morphik-1.2.2}/morphik/models.py +0 -0
  19. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/README.md +0 -0
  20. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/__init__.py +0 -0
  21. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/example_usage.py +0 -0
  22. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_docs/sample1.txt +0 -0
  23. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_docs/sample2.txt +0 -0
  24. {morphik-1.2.0 → morphik-1.2.2}/morphik/tests/test_docs/sample3.txt +0 -0
@@ -59,4 +59,5 @@ multi_vector_embeddings_*.json
59
59
  morphik_rust/target/
60
60
  morphik_rust/Cargo.lock
61
61
  DOCS_BY_FACILITIES/
62
- eval_freshworks/
62
+ eval_freshworks/
63
+ iep_test/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphik
3
- Version: 1.2.0
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
@@ -14,4 +14,4 @@ __all__ = [
14
14
  "DocumentQueryResponse",
15
15
  ]
16
16
 
17
- __version__ = "1.2.0"
17
+ __version__ = "1.2.2"
@@ -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.split("@")
69
- _, self._auth_token = auth.split(":")
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", org_id="org")
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", "org_id": "org"}
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", user_id="u1", expiry_days=30)
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", "user_id": "u1", "expiry_days": 30}
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()