datasourcelib 0.1.10__tar.gz → 0.1.12__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.
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/PKG-INFO +1 -1
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/setup.py +1 -1
- datasourcelib-0.1.12/src/datasourcelib/datasources/azure_devops_source.py +402 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib.egg-info/PKG-INFO +1 -1
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib.egg-info/SOURCES.txt +1 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/LICENSE +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/MANIFEST.in +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/README.md +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/pyproject.toml +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/setup.cfg +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/__init__.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/core/__init__.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/core/sync_base.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/core/sync_manager.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/core/sync_types.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/__init__.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/azure_devops_source copy.py +0 -0
- /datasourcelib-0.1.10/src/datasourcelib/datasources/azure_devops_source.py → /datasourcelib-0.1.12/src/datasourcelib/datasources/azure_devops_source10dec.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/blob_source.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/datasource_base.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/datasource_types.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/dataverse_source.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/sharepoint_source - Copy.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/sharepoint_source.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/sql_source.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/sql_source_bkup.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/indexes/__init__.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/indexes/azure_search_index.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/strategies/__init__.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/strategies/daily_load.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/strategies/full_load.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/strategies/incremental_load.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/strategies/ondemand_load.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/strategies/timerange_load.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/utils/__init__.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/utils/aggregation.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/utils/byte_reader.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/utils/exceptions.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/utils/file_reader.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/utils/logger.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/utils/validators.py +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib.egg-info/dependency_links.txt +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib.egg-info/requires.txt +0 -0
- {datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from datasourcelib.datasources.datasource_base import DataSourceBase
|
|
3
|
+
from datasourcelib.utils.logger import get_logger
|
|
4
|
+
from datasourcelib.utils.validators import require_keys
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
import regex as re
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import requests # type: ignore
|
|
14
|
+
except Exception:
|
|
15
|
+
requests = None # lazy import handled at runtime
|
|
16
|
+
|
|
17
|
+
class AzureDevOpsSource(DataSourceBase):
|
|
18
|
+
|
|
19
|
+
def validate_config(self) -> bool:
|
|
20
|
+
try:
|
|
21
|
+
require_keys(self.config, ["ado_organization", "ado_personal_access_token"])
|
|
22
|
+
return True
|
|
23
|
+
except Exception as ex:
|
|
24
|
+
logger.error("AzureDevOpsSource.validate_config: %s", ex)
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
def connect(self) -> bool:
|
|
28
|
+
if requests is None:
|
|
29
|
+
raise RuntimeError("requests package is required for AzureDevOpsSource")
|
|
30
|
+
# No persistent connection; store auth header
|
|
31
|
+
pat = self.config.get("ado_personal_access_token")
|
|
32
|
+
token = pat
|
|
33
|
+
token_b64 = base64.b64encode(token.encode("utf-8")).decode("utf-8")
|
|
34
|
+
self._headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
35
|
+
self._connected = True
|
|
36
|
+
logger.info("AzureDevOpsSource ready (no persistent connection required)")
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def sanitize(s: str) -> str:
|
|
41
|
+
"""Keep only A-Z a-z 0-9 underscore/dash/equals in a safe way."""
|
|
42
|
+
# using the `regex` import already present as `re`
|
|
43
|
+
return re.sub(r'[^A-Za-z0-9_\-=]', '', s)
|
|
44
|
+
|
|
45
|
+
def disconnect(self) -> None:
|
|
46
|
+
self._headers = {}
|
|
47
|
+
self._connected = False
|
|
48
|
+
logger.info("AzureDevOpsSource cleared")
|
|
49
|
+
|
|
50
|
+
def fetch_query_data(self, query: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]:
|
|
51
|
+
if requests is None:
|
|
52
|
+
raise RuntimeError("requests package is required for AzureDevOpsSource")
|
|
53
|
+
if not getattr(self, "_connected", False):
|
|
54
|
+
self.connect()
|
|
55
|
+
|
|
56
|
+
org = self.config.get("ado_organization")
|
|
57
|
+
project = self.config.get("ado_project")
|
|
58
|
+
query_id = self.config.get("ado_query_id")
|
|
59
|
+
api_version = self.config.get("api_version", "7.1")
|
|
60
|
+
if not query_id:
|
|
61
|
+
raise ValueError("AzureDevOpsSource.fetch_data requires 'query_id' or query argument")
|
|
62
|
+
|
|
63
|
+
base = f"https://dev.azure.com/{org}/"
|
|
64
|
+
if project:
|
|
65
|
+
base = f"{base}{project}/"
|
|
66
|
+
# WIQL query by id (returns list of work item refs)
|
|
67
|
+
wiql_url = f"{base}_apis/wit/wiql/{query_id}"
|
|
68
|
+
params = {"api-version": api_version}
|
|
69
|
+
method = self.config.get("method", "GET").upper()
|
|
70
|
+
query_response = requests.request(method, wiql_url, headers=getattr(self, "_headers", {}), params=params)
|
|
71
|
+
query_response.raise_for_status()
|
|
72
|
+
|
|
73
|
+
if query_response.status_code != 200:
|
|
74
|
+
raise RuntimeError(f"Error: {query_response.status_code}")
|
|
75
|
+
|
|
76
|
+
work_items_refs = query_response.json().get('workItems', []) or []
|
|
77
|
+
if not work_items_refs:
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
# collect ids and fetch details in batch to get all fields for all work item types
|
|
81
|
+
ids = [str(item.get('id')) for item in work_items_refs if item.get('id')]
|
|
82
|
+
if not ids:
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
details_url = f"https://dev.azure.com/{org}/{project}/_apis/wit/workitems"
|
|
86
|
+
# expand=all to include fields, relations, and attachments
|
|
87
|
+
params = {
|
|
88
|
+
"ids": ",".join(ids),
|
|
89
|
+
"api-version": api_version,
|
|
90
|
+
"$expand": "all"
|
|
91
|
+
}
|
|
92
|
+
details_resp = requests.get(details_url, headers=getattr(self, "_headers", {}), params=params)
|
|
93
|
+
details_resp.raise_for_status()
|
|
94
|
+
items = details_resp.json().get("value", [])
|
|
95
|
+
|
|
96
|
+
work_item_details: List[Dict[str, Any]] = []
|
|
97
|
+
for item in items:
|
|
98
|
+
item_id = item.get("id")
|
|
99
|
+
fields = item.get("fields", {}) or {}
|
|
100
|
+
|
|
101
|
+
# Normalize field keys to safe snake_case-like keys
|
|
102
|
+
norm_fields: Dict[str, Any] = {}
|
|
103
|
+
for k, v in fields.items():
|
|
104
|
+
nk = k.replace(".", "_")
|
|
105
|
+
nk = nk.lower()
|
|
106
|
+
norm_fields[nk] = v
|
|
107
|
+
|
|
108
|
+
# Helper to safely extract nested displayName for assigned to
|
|
109
|
+
assigned = norm_fields.get("system_assignedto")
|
|
110
|
+
if isinstance(assigned, dict):
|
|
111
|
+
assigned_to = assigned.get("displayName") or assigned.get("uniqueName") or str(assigned)
|
|
112
|
+
else:
|
|
113
|
+
assigned_to = assigned
|
|
114
|
+
|
|
115
|
+
# find a description-like field (some types use different field names)
|
|
116
|
+
desc = ""
|
|
117
|
+
for fk in ["system_description", "microsoft_vsts_createdby", "html_description"]:
|
|
118
|
+
if fk in norm_fields:
|
|
119
|
+
desc = norm_fields.get(fk) or ""
|
|
120
|
+
break
|
|
121
|
+
if not desc:
|
|
122
|
+
# fallback: first field key that contains 'description'
|
|
123
|
+
for kf, vf in norm_fields.items():
|
|
124
|
+
if "description" in kf and vf:
|
|
125
|
+
desc = vf
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
# clean HTML description to text
|
|
129
|
+
try:
|
|
130
|
+
c_desc = BeautifulSoup(desc or "", "html.parser").get_text()
|
|
131
|
+
except Exception:
|
|
132
|
+
c_desc = desc or ""
|
|
133
|
+
|
|
134
|
+
# Build common convenience values (use available fields)
|
|
135
|
+
wi_type = norm_fields.get("system_workitemtype") or norm_fields.get("system_witype") or ""
|
|
136
|
+
title = norm_fields.get("system_title") or ""
|
|
137
|
+
status = norm_fields.get("system_state") or ""
|
|
138
|
+
created = norm_fields.get("system_createddate") or norm_fields.get("system_created") or ""
|
|
139
|
+
changed = norm_fields.get("system_changeddate") or norm_fields.get("system_changed") or ""
|
|
140
|
+
tags = norm_fields.get("system_tags", "")
|
|
141
|
+
project_name = norm_fields.get("custom.projectname") or norm_fields.get("system_teamproject") or ""
|
|
142
|
+
|
|
143
|
+
rtype = norm_fields.get("custom.releasetype") or norm_fields.get("custom_releasetype") or ""
|
|
144
|
+
target_date = norm_fields.get("microsoft_vsts_scheduling_targetdate") or norm_fields.get("microsoft.vsts.scheduling.targetdate") or ""
|
|
145
|
+
|
|
146
|
+
# Construct a 'full' description string using available pieces
|
|
147
|
+
parts = []
|
|
148
|
+
if wi_type:
|
|
149
|
+
parts.append(f"{wi_type} ID {item_id}")
|
|
150
|
+
else:
|
|
151
|
+
parts.append(f"WorkItem {item_id}")
|
|
152
|
+
if created:
|
|
153
|
+
parts.append(f"was created on {created}")
|
|
154
|
+
if title:
|
|
155
|
+
parts.append(f"and has Title '{title}'")
|
|
156
|
+
if status:
|
|
157
|
+
parts.append(f"is currently in {status} state")
|
|
158
|
+
if assigned_to:
|
|
159
|
+
parts.append(f"is assigned to {assigned_to}")
|
|
160
|
+
if project_name:
|
|
161
|
+
parts.append(f"for Project '{project_name}'")
|
|
162
|
+
if rtype:
|
|
163
|
+
parts.append(f"release type '{rtype}'")
|
|
164
|
+
if target_date:
|
|
165
|
+
parts.append(f"with target date '{target_date}'")
|
|
166
|
+
if tags:
|
|
167
|
+
parts.append(f"Tags: {tags}")
|
|
168
|
+
if c_desc:
|
|
169
|
+
parts.append(f"Description: [{c_desc}]")
|
|
170
|
+
fullfeature = ". ".join(parts)
|
|
171
|
+
|
|
172
|
+
# include all normalized fields in the returned object for completeness
|
|
173
|
+
entry = {
|
|
174
|
+
"id": item_id,
|
|
175
|
+
"type": wi_type,
|
|
176
|
+
"title": title,
|
|
177
|
+
"status": status,
|
|
178
|
+
"assigned_to": assigned_to,
|
|
179
|
+
"created": created,
|
|
180
|
+
"changed_date": changed,
|
|
181
|
+
"tags": tags,
|
|
182
|
+
"project": project_name,
|
|
183
|
+
"release_type": rtype,
|
|
184
|
+
"target_date": target_date,
|
|
185
|
+
"description": c_desc,
|
|
186
|
+
"full": fullfeature
|
|
187
|
+
}
|
|
188
|
+
work_item_details.append(entry)
|
|
189
|
+
|
|
190
|
+
return work_item_details
|
|
191
|
+
|
|
192
|
+
def fetch_wiki_data(self, wiki_name: Optional[str] = None, max_depth: int = 3, **kwargs) -> List[Dict[str, Any]]:
|
|
193
|
+
"""
|
|
194
|
+
Crawl wiki pages in the configured Azure DevOps organization/project and return a list of
|
|
195
|
+
dicts: {"display_name": str, "url": str, "content": str, "wiki": str, "project": str}.
|
|
196
|
+
- wiki_name: optional filter to select a single wiki by name
|
|
197
|
+
- max_depth: how many child levels to traverse (>=1)
|
|
198
|
+
- If ado_project is configured, only fetch wikis from that project.
|
|
199
|
+
- Otherwise, fetch wikis from all projects in the organization.
|
|
200
|
+
"""
|
|
201
|
+
if requests is None:
|
|
202
|
+
raise RuntimeError("requests package is required for AzureDevOpsSource")
|
|
203
|
+
if not getattr(self, "_connected", False):
|
|
204
|
+
self.connect()
|
|
205
|
+
|
|
206
|
+
org = self.config.get("ado_organization")
|
|
207
|
+
configured_project = self.config.get("ado_project") # Rename to avoid overwriting in loop
|
|
208
|
+
api_version = self.config.get("api_version", "7.1")
|
|
209
|
+
headers = getattr(self, "_headers", {})
|
|
210
|
+
|
|
211
|
+
results: List[Dict[str, Any]] = []
|
|
212
|
+
seen_paths = set()
|
|
213
|
+
|
|
214
|
+
# Determine which projects to process
|
|
215
|
+
projects_to_process = []
|
|
216
|
+
if configured_project:
|
|
217
|
+
# Use only the configured project
|
|
218
|
+
projects_to_process = [configured_project]
|
|
219
|
+
logger.info("fetch_wiki_data: Using configured project: %s", configured_project)
|
|
220
|
+
else:
|
|
221
|
+
# Fetch all projects in the organization
|
|
222
|
+
try:
|
|
223
|
+
projects_url = f"https://dev.azure.com/{org}/_apis/projects?api-version={api_version}"
|
|
224
|
+
proj_resp = requests.get(projects_url, headers=headers, timeout=30)
|
|
225
|
+
proj_resp.raise_for_status()
|
|
226
|
+
proj_json = proj_resp.json()
|
|
227
|
+
projects_list = proj_json.get("value", [])
|
|
228
|
+
projects_to_process = [p.get("name") or p.get("id") for p in projects_list if p.get("name") or p.get("id")]
|
|
229
|
+
logger.info("fetch_wiki_data: Found %d projects in organization", len(projects_to_process))
|
|
230
|
+
except Exception as ex:
|
|
231
|
+
logger.exception("Failed to list projects in organization: %s", ex)
|
|
232
|
+
return []
|
|
233
|
+
|
|
234
|
+
# Process each project
|
|
235
|
+
for project_name in projects_to_process:
|
|
236
|
+
logger.info("fetch_wiki_data: Processing project: %s", project_name)
|
|
237
|
+
|
|
238
|
+
# 1) List wikis in this project
|
|
239
|
+
wikis_url = f"https://dev.azure.com/{org}/{project_name}/_apis/wiki/wikis?api-version={api_version}"
|
|
240
|
+
try:
|
|
241
|
+
resp = requests.get(wikis_url, headers=headers, timeout=30)
|
|
242
|
+
resp.raise_for_status()
|
|
243
|
+
wikis_json = resp.json()
|
|
244
|
+
wikis = wikis_json.get("value", []) if isinstance(wikis_json, dict) else []
|
|
245
|
+
except Exception as ex:
|
|
246
|
+
logger.warning("Failed to list wikis for project %s: %s", project_name, ex)
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# Filter selected wikis by name if specified
|
|
250
|
+
selected_wikis = []
|
|
251
|
+
for w in wikis:
|
|
252
|
+
name = w.get("name") or w.get("wikiName") or ""
|
|
253
|
+
if wiki_name:
|
|
254
|
+
if name.lower() == wiki_name.lower():
|
|
255
|
+
selected_wikis.append(w)
|
|
256
|
+
else:
|
|
257
|
+
# Include all wikis for this project
|
|
258
|
+
selected_wikis.append(w)
|
|
259
|
+
|
|
260
|
+
if not selected_wikis:
|
|
261
|
+
logger.debug("No wikis found in project %s matching filter (wiki_name=%s)", project_name, wiki_name)
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
# 2) Crawl pages in each wiki
|
|
265
|
+
for wiki in selected_wikis:
|
|
266
|
+
wiki_id = wiki.get("id") or wiki.get("name")
|
|
267
|
+
wiki_display = wiki.get("name") or wiki.get("wikiName") or str(wiki_id)
|
|
268
|
+
logger.info("fetch_wiki_data: Crawling wiki '%s' in project '%s'", wiki_display, project_name)
|
|
269
|
+
|
|
270
|
+
# BFS queue of (path, depth). Start at root path "/"
|
|
271
|
+
queue = [("/", 1)]
|
|
272
|
+
|
|
273
|
+
while queue:
|
|
274
|
+
path, depth = queue.pop(0)
|
|
275
|
+
if depth > max_depth:
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
# Pages listing for this path with recursionLevel=1 to get direct children
|
|
279
|
+
pages_url = (
|
|
280
|
+
f"https://dev.azure.com/{org}/{project_name}/_apis/wiki/wikis/{wiki_id}/pages"
|
|
281
|
+
f"?path={path}&recursionLevel=1&api-version={api_version}"
|
|
282
|
+
)
|
|
283
|
+
try:
|
|
284
|
+
p_resp = requests.get(pages_url, headers=headers, timeout=30)
|
|
285
|
+
p_resp.raise_for_status()
|
|
286
|
+
p_json = p_resp.json()
|
|
287
|
+
pages = p_json.get("value") or p_json.get("subPages") or []
|
|
288
|
+
except Exception as ex:
|
|
289
|
+
logger.warning("Failed to list pages for wiki %s path %s in project %s: %s",
|
|
290
|
+
wiki_display, path, project_name, ex)
|
|
291
|
+
pages = []
|
|
292
|
+
|
|
293
|
+
for page in pages:
|
|
294
|
+
page_path = page.get("path") or "/"
|
|
295
|
+
# Dedupe by wiki id + project + path
|
|
296
|
+
key = f"{project_name}:{wiki_id}:{page_path}"
|
|
297
|
+
if key in seen_paths:
|
|
298
|
+
continue
|
|
299
|
+
seen_paths.add(key)
|
|
300
|
+
|
|
301
|
+
# Display name and url
|
|
302
|
+
display_name = page.get("name") or page.get("pageName") or page_path.strip("/") or "/"
|
|
303
|
+
new_display_name = self.sanitize(display_name.replace(" ", "_").strip()),
|
|
304
|
+
url = (
|
|
305
|
+
page.get("remoteUrl")
|
|
306
|
+
or page.get("url")
|
|
307
|
+
or (page.get("_links") or {}).get("web", {}).get("href")
|
|
308
|
+
or ""
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Fetch page content (includeContent)
|
|
312
|
+
content_text = ""
|
|
313
|
+
try:
|
|
314
|
+
content_url = (
|
|
315
|
+
f"https://dev.azure.com/{org}/{project_name}/_apis/wiki/wikis/{wiki_id}/pages"
|
|
316
|
+
f"?path={page_path}&includeContent=true&api-version={api_version}"
|
|
317
|
+
)
|
|
318
|
+
c_resp = requests.get(content_url, headers=headers, timeout=30)
|
|
319
|
+
c_resp.raise_for_status()
|
|
320
|
+
c_json = c_resp.json()
|
|
321
|
+
|
|
322
|
+
# Page content may be in several places depending on API version
|
|
323
|
+
if isinstance(c_json, dict):
|
|
324
|
+
# If API returns page object
|
|
325
|
+
content_text = (
|
|
326
|
+
c_json.get("content")
|
|
327
|
+
or (c_json.get("value", [{}])[0].get("content", "") if c_json.get("value") else "")
|
|
328
|
+
or c_json.get("text", "")
|
|
329
|
+
)
|
|
330
|
+
else:
|
|
331
|
+
# Fallback to raw bytes
|
|
332
|
+
content_text = c_resp.content.decode("utf-8", errors="ignore")
|
|
333
|
+
except Exception as fetch_ex:
|
|
334
|
+
logger.debug("Failed to fetch content for page %s: %s", display_name, fetch_ex)
|
|
335
|
+
# Best-effort fallback: try to GET the web url (may return HTML)
|
|
336
|
+
if url:
|
|
337
|
+
try:
|
|
338
|
+
w_resp = requests.get(url, headers=headers, timeout=30)
|
|
339
|
+
w_resp.raise_for_status()
|
|
340
|
+
content_text = w_resp.content.decode("utf-8", errors="ignore")
|
|
341
|
+
except Exception:
|
|
342
|
+
content_text = ""
|
|
343
|
+
# Construct a 'full' description string using available pieces
|
|
344
|
+
content_text = BeautifulSoup(content_text or "", "html.parser").get_text(),
|
|
345
|
+
parts = []
|
|
346
|
+
if new_display_name:
|
|
347
|
+
parts.append(f"Wiki Page Name is {display_name}. Page has information about {display_name}")
|
|
348
|
+
if project_name:
|
|
349
|
+
parts.append(f"This page is documented by for Project '{project_name}' and by the team '{project_name}'")
|
|
350
|
+
if url:
|
|
351
|
+
parts.append(f"The devops wiki page (url) link to access this page is {url}")
|
|
352
|
+
if project_name:
|
|
353
|
+
parts.append(f"These wiki page content refers sharepoint site links and other documents from sharepoint. So to get full detailed steps or contents you need to refer those links with appropriate permissions. This page contents are available on wiki are [{content_text}].")
|
|
354
|
+
|
|
355
|
+
index_content = ". ".join(parts)
|
|
356
|
+
results.append({
|
|
357
|
+
"display_name": new_display_name,
|
|
358
|
+
"url": url,
|
|
359
|
+
"content": index_content,
|
|
360
|
+
"project": project_name
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
# Enqueue child pages
|
|
364
|
+
if depth < max_depth:
|
|
365
|
+
# If page has children field, use it
|
|
366
|
+
children = page.get("children") or []
|
|
367
|
+
if children:
|
|
368
|
+
for ch in children:
|
|
369
|
+
ch_path = ch.get("path") or ch
|
|
370
|
+
queue.append((ch_path, depth + 1))
|
|
371
|
+
else:
|
|
372
|
+
# Fallback: attempt to list sub-path under current page path
|
|
373
|
+
sub_path = page_path.rstrip("/") + "/"
|
|
374
|
+
queue.append((sub_path, depth + 1))
|
|
375
|
+
|
|
376
|
+
logger.info("fetch_wiki_data completed: Retrieved %d wiki pages", len(results))
|
|
377
|
+
return results
|
|
378
|
+
|
|
379
|
+
def fetch_data(self, query: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]:
|
|
380
|
+
"""
|
|
381
|
+
Dispatch fetch call to either wiki downloader or WIQL/query fetcher.
|
|
382
|
+
|
|
383
|
+
Priority:
|
|
384
|
+
1. kwargs['ado_download_wiki'] if provided
|
|
385
|
+
2. self.config['ado_download_wiki'] otherwise
|
|
386
|
+
|
|
387
|
+
Accepts same params as fetch_query_data / fetch_wiki_data and returns their output.
|
|
388
|
+
"""
|
|
389
|
+
# Determine flag from kwargs first, then config
|
|
390
|
+
download_flag = kwargs.pop("ado_download_wiki", None)
|
|
391
|
+
if download_flag is None:
|
|
392
|
+
download_flag = self.config.get("ado_download_wiki", False)
|
|
393
|
+
|
|
394
|
+
# normalize boolean-like strings
|
|
395
|
+
if isinstance(download_flag, str):
|
|
396
|
+
download_flag = download_flag.strip().lower() in ("1", "true", "yes", "y", "on")
|
|
397
|
+
|
|
398
|
+
if download_flag:
|
|
399
|
+
# pass query as wiki_name if caller intended, otherwise kwargs forwarded
|
|
400
|
+
return self.fetch_wiki_data(wiki_name=query, **kwargs)
|
|
401
|
+
else:
|
|
402
|
+
return self.fetch_query_data(query=query, **kwargs)
|
|
@@ -16,6 +16,7 @@ src/datasourcelib/core/sync_types.py
|
|
|
16
16
|
src/datasourcelib/datasources/__init__.py
|
|
17
17
|
src/datasourcelib/datasources/azure_devops_source copy.py
|
|
18
18
|
src/datasourcelib/datasources/azure_devops_source.py
|
|
19
|
+
src/datasourcelib/datasources/azure_devops_source10dec.py
|
|
19
20
|
src/datasourcelib/datasources/blob_source.py
|
|
20
21
|
src/datasourcelib/datasources/datasource_base.py
|
|
21
22
|
src/datasourcelib/datasources/datasource_types.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/datasource_base.py
RENAMED
|
File without changes
|
{datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/datasource_types.py
RENAMED
|
File without changes
|
{datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/dataverse_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/sharepoint_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/datasources/sql_source_bkup.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/indexes/azure_search_index.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/strategies/incremental_load.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib/strategies/timerange_load.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasourcelib-0.1.10 → datasourcelib-0.1.12}/src/datasourcelib.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|