rosetta-cli 3.0.5__tar.gz → 3.0.6__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 (33) hide show
  1. {rosetta_cli-3.0.5/rosetta_cli.egg-info → rosetta_cli-3.0.6}/PKG-INFO +1 -1
  2. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/pyproject.toml +1 -1
  3. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/commands/cleanup_command.py +10 -1
  4. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/commands/parse_command.py +43 -44
  5. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/rosetta_config.py +49 -15
  6. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/rosetta_publisher.py +8 -3
  7. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6/rosetta_cli.egg-info}/PKG-INFO +1 -1
  8. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/MANIFEST.in +0 -0
  9. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/README.md +0 -0
  10. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/env.template +0 -0
  11. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/__init__.py +0 -0
  12. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/__main__.py +0 -0
  13. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/cli.py +0 -0
  14. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/commands/__init__.py +0 -0
  15. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/commands/base_command.py +0 -0
  16. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/commands/list_command.py +0 -0
  17. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/commands/publish_command.py +0 -0
  18. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/commands/verify_command.py +0 -0
  19. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/ragflow_client.py +0 -0
  20. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/rosetta_auth.py +0 -0
  21. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/rosetta_utils.py +0 -0
  22. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/services/__init__.py +0 -0
  23. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/services/auth_service.py +0 -0
  24. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/services/dataset_service.py +0 -0
  25. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/services/document_data.py +0 -0
  26. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/services/document_service.py +0 -0
  27. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli/typing_utils.py +0 -0
  28. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli.egg-info/SOURCES.txt +0 -0
  29. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli.egg-info/dependency_links.txt +0 -0
  30. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli.egg-info/entry_points.txt +0 -0
  31. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli.egg-info/requires.txt +0 -0
  32. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/rosetta_cli.egg-info/top_level.txt +0 -0
  33. {rosetta_cli-3.0.5 → rosetta_cli-3.0.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rosetta-cli
3
- Version: 3.0.5
3
+ Version: 3.0.6
4
4
  Summary: Rosetta CLI for publishing knowledge base content to RAGFlow
5
5
  Author-email: Rosetta Team <rosetta-support@griddynamics.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rosetta-cli"
7
- version = "3.0.5"
7
+ version = "3.0.6"
8
8
  description = "Rosetta CLI for publishing knowledge base content to RAGFlow"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -87,7 +87,16 @@ class CleanupCommand(BaseCommand):
87
87
  dataset_service: DatasetService,
88
88
  args: CommandArgs,
89
89
  ) -> list[DocumentLike]:
90
- """Get documents to delete based on filters."""
90
+ """Get documents to delete based on filters.
91
+
92
+ Accepted limitation: every branch below reads one page, bounded by
93
+ RAGFLOW_PAGE_SIZE (default 1000). On a dataset larger than that, this
94
+ command deletes the documents on the first page and reports success -
95
+ the remainder is left in place with no warning. The instruction set is
96
+ not expected to approach the cap, and this command serves the optional
97
+ MCP publishing pipeline rather than plugin delivery. Anyone pointing
98
+ `--dataset` at a larger dataset should know the delete is partial.
99
+ """
91
100
  document_service = DocumentService(self.client)
92
101
 
93
102
  if args.tags:
@@ -11,7 +11,7 @@ from ..services.auth_service import AuthService
11
11
 
12
12
 
13
13
  from .base_command import BaseCommand
14
- from ..typing_utils import CommandArgs, DatasetLike, JsonDict
14
+ from ..typing_utils import CommandArgs, DatasetLike, DocumentLike, JsonDict
15
15
 
16
16
 
17
17
  class ParseCommand(BaseCommand):
@@ -102,58 +102,57 @@ class ParseCommand(BaseCommand):
102
102
  traceback.print_exc()
103
103
  return 1
104
104
 
105
+ # RAGFlow `run` values that mean a document still needs parsing.
106
+ PARSE_REQUIRED_STATUSES = ("FAIL", "UNSTART", "CANCEL")
107
+
105
108
  def _get_documents_to_parse(self, dataset: DatasetLike, args: CommandArgs) -> tuple[list[JsonDict], dict[str, int]]:
106
109
  """Get documents that need parsing."""
107
- document_service = DocumentService(self.client)
108
110
  docs_to_parse: list[JsonDict] = []
109
111
  status_counts = {'done': 0, 'running': 0}
110
112
 
111
113
  print(f"Checking parsing status for documents...")
112
114
 
115
+ # One unfiltered list call serves both modes: force selects everything,
116
+ # default partitions the same list client-side on `run`. Default mode used
117
+ # to make two calls - a server-side run= filter, plus a full list purely to
118
+ # tally done/running for the summary.
119
+ #
120
+ # Deliberate tradeoff, not a no-op: the server-filtered call surfaced up to
121
+ # page_size documents *needing parsing*, whereas page 1 of an unfiltered
122
+ # list surfaces only the needing-parse documents that happen to fall inside
123
+ # it. Once a dataset exceeds RAGFLOW_PAGE_SIZE this can therefore select
124
+ # fewer documents than before.
125
+ #
126
+ # Accepted limitation, not a deferred fix: every list path in this command
127
+ # reads page 1 only, bounded by RAGFLOW_PAGE_SIZE (default 1000). The
128
+ # instruction set is not expected to approach that cap - it is roughly 470
129
+ # files - and this command serves the optional MCP publishing pipeline,
130
+ # which is secondary to plugin delivery. The page-1-only read is also
131
+ # pre-existing: it already applied to the force branch and to the
132
+ # done/running tally.
133
+ documents = dataset.list_documents(page_size=self.config.page_size)
134
+
135
+ selected: list[tuple[DocumentLike, str]] = []
136
+ for document in documents:
137
+ status = self._count_parse_status(status_counts, document)
138
+ if args.force or status in self.PARSE_REQUIRED_STATUSES:
139
+ selected.append((document, status))
140
+
113
141
  if args.force:
114
- # Force mode: all documents
115
- documents = dataset.list_documents(page_size=self.config.page_size)
116
-
117
- print(f"Found {len(documents)} document(s) (force mode - parsing all)\n")
118
-
119
- for document in documents:
120
- status = self._count_parse_status(status_counts, document)
121
- doc_id = getattr(document, 'id', None)
122
- if doc_id:
123
- docs_to_parse.append({
124
- "id": doc_id,
125
- "name": getattr(document, 'name', 'Untitled'),
126
- "dataset_id": dataset.id,
127
- "folder": ".",
128
- "status": status
129
- })
142
+ print(f"Found {len(selected)} document(s) (force mode - parsing all)\n")
130
143
  else:
131
- # Default mode: filter by status (need parsing)
132
- documents_needing_parse = document_service.list_documents_by_status(
133
- dataset,
134
- statuses=["FAIL", "UNSTART", "CANCEL"],
135
- limit=self.config.page_size
136
- )
137
-
138
- # Tally skipped documents from the full list so the summary reflects
139
- # what this run will not re-trigger.
140
- all_documents = dataset.list_documents(page_size=self.config.page_size)
141
- for document in all_documents:
142
- self._count_parse_status(status_counts, document)
143
-
144
- print(f"Found {len(documents_needing_parse)} document(s) needing parsing\n")
145
-
146
- for document in documents_needing_parse:
147
- status = self._count_parse_status(status_counts, document)
148
- doc_id = getattr(document, 'id', None)
149
- if doc_id:
150
- docs_to_parse.append({
151
- "id": doc_id,
152
- "name": getattr(document, 'name', 'Untitled'),
153
- "dataset_id": dataset.id,
154
- "folder": ".",
155
- "status": status
156
- })
144
+ print(f"Found {len(selected)} document(s) needing parsing\n")
145
+
146
+ for document, status in selected:
147
+ doc_id = getattr(document, 'id', None)
148
+ if doc_id:
149
+ docs_to_parse.append({
150
+ "id": doc_id,
151
+ "name": getattr(document, 'name', 'Untitled'),
152
+ "dataset_id": dataset.id,
153
+ "folder": ".",
154
+ "status": status
155
+ })
157
156
 
158
157
  return docs_to_parse, status_counts
159
158
 
@@ -9,7 +9,6 @@ import logging
9
9
  import os
10
10
  from dataclasses import dataclass
11
11
  from pathlib import Path
12
- from urllib.parse import urlparse
13
12
 
14
13
  from dotenv import load_dotenv
15
14
 
@@ -69,14 +68,40 @@ def find_env_file(env_name: str | None = None) -> Path | None:
69
68
  return None
70
69
 
71
70
 
72
- def _first_subdomain(url: str) -> str | None:
73
- """Extract first subdomain from URL. 'https://ims-dev.example.com/' -> 'ims-dev'."""
74
- try:
75
- hostname = urlparse(url).hostname or ""
76
- parts = hostname.split(".")
77
- return parts[0] if len(parts) > 1 else None
78
- except Exception:
79
- return None
71
+ # Environment keywords searched for in the RAGFlow URL, in scan order.
72
+ #
73
+ # ORDER IS LOAD-BEARING - do not sort or alphabetize this tuple. The scan takes the
74
+ # first keyword that appears anywhere in the URL, so a keyword that contains another
75
+ # keyword must be listed before the one it contains. "prod" is a substring of
76
+ # "preprod" - the only such pair in this set - so putting "prod" first would make
77
+ # preprod.example.com resolve to "prod". test_environment_keyword_order_is_safe in
78
+ # tests/test_rosetta_config_environment.py enforces this invariant.
79
+ ENVIRONMENT_KEYWORDS: tuple[str, ...] = (
80
+ "preprod", # must precede "prod", which it contains
81
+ "prod",
82
+ "stag",
83
+ "qa",
84
+ "dev",
85
+ "test",
86
+ "uat",
87
+ "sandbox",
88
+ "perf",
89
+ )
90
+
91
+
92
+ def _environment_from_url(url: str) -> str | None:
93
+ """Detect an environment name from keywords in a RAGFlow URL.
94
+
95
+ Case-insensitive substring scan over the URL exactly as given (scheme, host,
96
+ port and path all count). The first keyword in ENVIRONMENT_KEYWORDS to appear
97
+ wins, and the resolved value is the keyword itself, not the surrounding label:
98
+ 'https://ims-dev.example.com/' -> 'dev'. Returns None when no keyword appears.
99
+ """
100
+ lowered = (url or "").lower()
101
+ for keyword in ENVIRONMENT_KEYWORDS:
102
+ if keyword in lowered:
103
+ return keyword
104
+ return None
80
105
 
81
106
 
82
107
  @dataclass
@@ -98,7 +123,12 @@ class RosettaConfig:
98
123
  RAGFLOW_PAGE_SIZE: Page size for listing operations (default: 1000)
99
124
  RAGFLOW_PARSE_TIMEOUT: Timeout for parsing operations in seconds (default: 1200)
100
125
  RAGFLOW_TIMEOUT: HTTP request timeout in seconds (default: 30)
101
- ENVIRONMENT: Environment name (default: "local")
126
+ ENVIRONMENT: Environment display label. Takes precedence over URL keyword
127
+ detection, but not over an explicit --env argument. When unset, the
128
+ label is detected from RAGFLOW_BASE_URL by a case-insensitive scan for
129
+ the first of preprod, prod, stag, qa, dev, test, uat, sandbox, perf to
130
+ appear in it (the resolved value is that keyword itself, so
131
+ "https://ims-dev.example.com" yields "dev"); "local" if none appear.
102
132
 
103
133
  Examples:
104
134
  >>> config = RosettaConfig.from_env(".env")
@@ -194,9 +224,11 @@ class RosettaConfig:
194
224
  Args:
195
225
  environment: Optional explicit environment name (e.g., "local",
196
226
  "dev", "remote"). If provided, this value is used and takes
197
- precedence over the ENVIRONMENT environment variable. If not
198
- provided, the ENVIRONMENT variable is used, defaulting to
199
- "local" when unset.
227
+ precedence over everything else. If not provided, the
228
+ ENVIRONMENT variable is used; failing that, the label is
229
+ detected from a keyword in RAGFLOW_BASE_URL (see
230
+ ENVIRONMENT_KEYWORDS), defaulting to "local" when no keyword
231
+ appears.
200
232
 
201
233
  Returns:
202
234
  RosettaConfig instance
@@ -213,9 +245,11 @@ class RosettaConfig:
213
245
  api_key = os.getenv("RAGFLOW_API_KEY", "")
214
246
  dataset_default = os.getenv("RAGFLOW_DATASET_DEFAULT", "aia")
215
247
  dataset_template = os.getenv("RAGFLOW_DATASET_TEMPLATE", "aia-{release}")
216
- # fallback to ENVIRONMENT env var, or default to "local"
248
+ # Environment is a display label only - nothing branches on its value.
249
+ # Precedence: explicit argument (--env) > ENVIRONMENT > keyword detected in
250
+ # the RAGFlow URL > "local".
217
251
  if not environment:
218
- environment = os.getenv("ENVIRONMENT") or _first_subdomain(base_url) or "local"
252
+ environment = os.getenv("ENVIRONMENT") or _environment_from_url(base_url) or "local"
219
253
 
220
254
  # Dataset creation settings
221
255
  embedding_model = os.getenv("RAGFLOW_EMBEDDING_MODEL") or None
@@ -641,13 +641,16 @@ class ContentPublisher:
641
641
  return name[: -len(path.name)] + new_name
642
642
 
643
643
  # Collect all docs to delete into one list: (doc, label)
644
- # Start with unmanaged: incomplete metadata (ims_doc_id or original_path absent)
644
+ # Start with managed docs carrying incomplete metadata: original_path is
645
+ # set (so the doc is ours) but ims_doc_id is absent. A doc with no
646
+ # original_path is custom/unmanaged and is never touched - see the
647
+ # docstring guarantee, which _cleanup_orphans enforces the same way.
645
648
  duplicates: list[tuple[DocumentLike, str]] = []
646
649
  for doc in all_docs:
647
650
  meta = getattr(doc, "meta_fields", {}) or {}
648
651
  ims_doc_id = meta.get("ims_doc_id") if isinstance(meta, dict) else getattr(meta, "ims_doc_id", None)
649
652
  doc_original_path = meta.get("original_path", "") if isinstance(meta, dict) else getattr(meta, "original_path", "")
650
- if not ims_doc_id or not doc_original_path:
653
+ if doc_original_path and not ims_doc_id:
651
654
  doc_name = getattr(doc, "name", "") or doc.id
652
655
  duplicates.append((doc, doc_name))
653
656
 
@@ -674,8 +677,10 @@ class ContentPublisher:
674
677
  duplicates.append((doc, original_path))
675
678
 
676
679
  # Name duplicates: foo.md + foo(1).md + foo(2).md ...
680
+ # Grouped over managed_docs, not all_docs: a custom doc whose stripped
681
+ # name happens to collide with a managed one must not be deleted.
677
682
  name_groups: dict[str, list[DocumentLike]] = defaultdict(list)
678
- for doc in all_docs:
683
+ for doc in managed_docs:
679
684
  doc_name = getattr(doc, "name", "") or ""
680
685
  if not doc_name:
681
686
  continue
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rosetta-cli
3
- Version: 3.0.5
3
+ Version: 3.0.6
4
4
  Summary: Rosetta CLI for publishing knowledge base content to RAGFlow
5
5
  Author-email: Rosetta Team <rosetta-support@griddynamics.com>
6
6
  License-Expression: Apache-2.0
File without changes
File without changes
File without changes
File without changes