Skip to content

Commit 4d4ce2b

Browse files
author
boonhapus
committed
Merge branch 'dev'
2 parents cd29463 + 615de87 commit 4d4ce2b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2277
-427
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,6 @@ dmypy.json
139139
dist/**/*.whl
140140
dist/**/*.zip
141141
dist/**/*.tar.gz
142+
143+
tmlout/*
144+
tmlvalidate/*

cs_tools/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.3.2'
1+
__version__ = '1.3.3'

cs_tools/api/_rest_api_v1.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
_Security,
1313
_Session,
1414
Data,
15+
Group,
1516
Logs,
1617
Metadata,
1718
Security,
@@ -32,7 +33,8 @@ def _secure_for_log(kw) -> Dict[str, Any]:
3233
except TypeError:
3334
secure = copy.deepcopy({k: v for k, v in kw.items() if k not in ('file', 'files')})
3435

35-
return secure.get('data', {}).pop('password', None)
36+
secure.get('data', {}).pop('password', None)
37+
return secure
3638

3739

3840
class _RESTAPIv1:
@@ -56,6 +58,7 @@ def __init__(self, config, ts):
5658

5759
# public API endpoints
5860
self.data = Data(self)
61+
self.group = Group(self)
5962
self.metadata = Metadata(self)
6063
self.security = Security(self)
6164
self.user = User(self)
@@ -75,6 +78,7 @@ def request(
7578
endpoint: str,
7679
*,
7780
privacy: str='public',
81+
timeout: int=-1,
7882
**kw
7983
) -> httpx.Response:
8084
"""
@@ -107,6 +111,10 @@ def request(
107111
**kw
108112
passed into the httpx.request call
109113
"""
114+
# "httpx.Client.timeout = None" has meaning, -1 does not.
115+
if timeout == -1:
116+
timeout = self._http.timeout
117+
110118
if httpx.URL(endpoint).is_relative_url:
111119
_privacy = {
112120
# IF NOT FOUND IN THIS MAPPING, THEN IT'S AN UNDOCUMENTED API
@@ -124,7 +132,7 @@ def request(
124132
log.debug(f'{method} >> {endpoint} with data:\n\tkwargs={_secure_for_log(kw)}')
125133

126134
meth = getattr(self._http, method.lower())
127-
r = meth(endpoint, **kw)
135+
r = meth(endpoint, **kw, timeout=timeout)
128136
r.raise_for_status()
129137
log.debug(f'<< HTTP: {r.status_code}')
130138

cs_tools/api/middlewares/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .connection import ConnectionMiddleware
2+
from .group import GroupMiddleware
23
from .metadata import MetadataMiddleware
34
from .pinboard import PinboardMiddleware
45
from .answer import AnswerMiddleware

cs_tools/api/middlewares/group.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import Any, Dict, List, Union
2+
import logging
3+
4+
from pydantic import validate_arguments
5+
6+
log = logging.getLogger(__name__)
7+
8+
9+
class GroupMiddleware:
10+
"""
11+
Functions to simplify using the group API
12+
"""
13+
def __init__(self, ts):
14+
self.ts = ts
15+
16+
@validate_arguments
17+
def get_group_id(self, name: str):
18+
"""
19+
Returns the GUID for a group with the given name.
20+
:param name: Name of the group, like 'Administrator'
21+
"""
22+
r = self.ts.api.group.get_group(name=name)
23+
info = r.json()
24+
return info['header']['id']

cs_tools/api/middlewares/metadata.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from cs_tools.data.enums import (
77
DownloadableContent, GUID, MetadataCategory, MetadataObject, MetadataObjectSubtype,
8-
PermissionType
98
)
109
from cs_tools.errors import ContentDoesNotExist
1110
from cs_tools.util import chunks
@@ -302,35 +301,54 @@ def get_edoc_object_list(self, guids: List[GUID]) -> List[Dict[str,str]]:
302301
return mapped_guids
303302

304303
@validate_arguments
305-
def get_object_ids_with_tags(self, tags: List[str]) -> List[Dict[str,str]]:
304+
def get_object_ids_with_tags_or_author(
305+
self, tags: List[str] = None,
306+
author: GUID = None,
307+
ignore_datasources: bool = False
308+
) -> List[Dict[str,str]]:
306309
"""
307-
Gets a list of IDs for the associated tag and returns as a list of object ID to type mapping.
310+
Gets a list of IDs for the associated tag and/or author and returns as a list of object ID to type mapping.
311+
Either the author or the tag must be specified. If both are specified, it will be the content owned by that
312+
person with the given tag.
308313
309314
The return format looks like:
310315
[
311316
{"id": "0291f1cd-5f8e-4d96-80e2-e5ef1aa6c44f", "type":"QUESTION_ANSWER_BOOK"},
312317
{"id": "4bcaadb4-031a-4afd-b159-2c0c0f194c42", "type":"PINBOARD_ANSWER_BOOK"}
313318
]
314319
:param tags: The list of tags to get ids for.
320+
:param author: The author of the content.
321+
:param ignore_datasources: If true, will ignore data sources.
315322
"""
316323

324+
# testing - remove later
317325
object_ids = []
318326
for metadata_type in DownloadableContent:
327+
if ignore_datasources and metadata_type == DownloadableContent.data_source:
328+
continue
329+
319330
offset = 0
320331

321332
while True:
322-
r = self.ts.api._metadata.list(type=metadata_type.value, batchsize=500, offset=offset, tagname=tags)
333+
r = self.ts.api._metadata.list(type=metadata_type.value, batchsize=500,
334+
offset=offset, tagname=tags, authorguid=author)
323335
data = r.json()
324336
offset += len(data)
325337

326338
for metadata in data['headers']:
327-
object_ids.append(metadata["id"])
339+
340+
if author and metadata['author'] == author: # workaround for 7.1/7.2
341+
object_ids.append(metadata["id"])
342+
elif not author: # no author specified, so just add it.
343+
object_ids.append(metadata["id"])
344+
# else just ignore it.
328345

329346
if data['isLastBatch']:
330347
break
331348

332349
return list(set(object_ids)) # might have been duplicates
333350

351+
334352
@classmethod
335353
@validate_arguments
336354
def map_subtype_to_type(self, subtype: Union[str, None]) -> str:
@@ -344,7 +362,7 @@ def map_subtype_to_type(self, subtype: Union[str, None]) -> str:
344362

345363
return subtype
346364

347-
def _lookup_geo_config(self, column_details) -> str:
365+
def _lookup_geo_config(self, column_details) -> Union[str, None]:
348366
try:
349367
config = column_details['geoConfig']
350368
except KeyError:
@@ -362,7 +380,7 @@ def _lookup_geo_config(self, column_details) -> str:
362380

363381
return 'Unknown'
364382

365-
def _lookup_calendar_guid(self, column_details) -> str:
383+
def _lookup_calendar_guid(self, column_details) -> Union[str, None]:
366384
try:
367385
ccal_guid = column_details['calendarTableGUID']
368386
except KeyError:
@@ -375,12 +393,13 @@ def _lookup_calendar_guid(self, column_details) -> str:
375393

376394
return self.cache['calendar_type'][ccal_guid]
377395

378-
def _lookup_currency_type(self, column_details) -> str:
396+
def _lookup_currency_type(self, column_details) -> Union[str, None]:
379397
try:
380398
currency_info = column_details['currencyTypeInfo']
381399
except KeyError:
382400
return None
383401

402+
name = None
384403
if currency_info['setting'] == 'FROM_USER_LOCALE':
385404
name = 'Infer From Browser'
386405
elif currency_info['setting'] == 'FROM_ISO_CODE':
@@ -396,3 +415,41 @@ def _lookup_currency_type(self, column_details) -> str:
396415
name = self.cache['currency_type'][g]
397416

398417
return name
418+
419+
@validate_arguments
420+
def objects_exist(self, metadata_type: MetadataObject, guids: List[GUID]) -> Dict[GUID, bool]:
421+
"""
422+
Checks if the list of objects exist.
423+
:param metadata_type: The type to check for. Must do one at a time.
424+
:param guids: The list of GUIDs to check.
425+
:return: A map of GUID to boolean, where True == it exists.
426+
"""
427+
r = self.ts.api.metadata.list_object_headers(type=metadata_type, fetchids=guids)
428+
content = r.json()
429+
430+
# The response is a list of objects that only include the ones that exist. So check each GUID and add to the
431+
# map.
432+
returned_ids = [obj.get("id") for obj in content]
433+
existence = {}
434+
for guid in guids:
435+
existence[guid] = guid in returned_ids
436+
437+
return existence
438+
439+
@classmethod
440+
@validate_arguments
441+
def tml_type_to_metadata_object(cls, tml_type: str) -> Union[MetadataObject, None]:
442+
"""
443+
Converts a tml type (e.g. "worksheet") to a MetadataObject type, (e.g. MetadataObject.logical_table)
444+
:param tml_type: The TML type, such as None
445+
"""
446+
mapping = {
447+
"table": MetadataObject.logical_table,
448+
"view": MetadataObject.logical_table,
449+
"worksheet": MetadataObject.logical_table,
450+
"pinboard": MetadataObject.pinboard,
451+
"liveboard": MetadataObject.pinboard,
452+
"answer": MetadataObject.saved_answer,
453+
}
454+
455+
return mapping.get(tml_type, None)

cs_tools/api/middlewares/tql.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def query(
5050
sample: int = 50,
5151
database: str = None,
5252
schema_: Annotated[str, Field(alias='schema')] = 'falcon_default_schema',
53-
http_timeout: int = 5.0
53+
http_timeout: int = 60.0
5454
) -> Tuple[List[Dict[str, str]], List[Dict[str, Any]]]:
5555
"""
5656
@@ -103,7 +103,7 @@ def command(
103103
database: str = None,
104104
schema_: str = 'falcon_default_schema',
105105
raise_errors: bool = False,
106-
http_timeout: int = 5.0
106+
http_timeout: int = 60.0
107107
) -> List[Dict[str, Any]]:
108108
"""
109109
"""
@@ -143,7 +143,7 @@ def script(
143143
fp: pathlib.Path,
144144
*,
145145
raise_errors: bool = False,
146-
http_timeout: int = 5.0
146+
http_timeout: int = 60.0
147147
) -> List[Dict[str, Any]]:
148148
"""
149149
"""

cs_tools/api/middlewares/tsload.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ def upload(
117117
self,
118118
fd: Union[BufferedIOBase, TextIOWrapper, _TemporaryFileWrapper],
119119
*,
120-
ignore_node_redirect: bool = False,
121120
database: str,
122121
table: str,
123122
schema_: str = 'falcon_default_schema',
@@ -134,6 +133,9 @@ def upload(
134133
escape_character: str = '"',
135134
enclosing_character: str = '"',
136135
flexible: bool = False,
136+
# not related to Remote TSLOAD API
137+
ignore_node_redirect: bool = False,
138+
http_timeout: int = 60.0
137139
) -> List[Dict[str, Any]]:
138140
"""
139141
Load a file via tsload on a remote server.
@@ -210,7 +212,7 @@ def upload(
210212
}
211213

212214
try:
213-
r = self.ts.api.ts_dataservice.load_init(flags)
215+
r = self.ts.api.ts_dataservice.load_init(flags, timeout=http_timeout)
214216
except Exception as e:
215217
raise TSLoadServiceUnreachable(
216218
f'[red]something went wrong trying to access tsload service: {e}[/]'
@@ -238,7 +240,7 @@ def upload(
238240
if not ignore_node_redirect:
239241
self._check_for_redirect_auth(data['cycle_id'])
240242

241-
self.ts.api.ts_dataservice.load_start(data['cycle_id'], fd=fd)
243+
self.ts.api.ts_dataservice.load_start(data['cycle_id'], fd=fd, timeout=http_timeout)
242244
self.ts.api.ts_dataservice.load_commit(data['cycle_id'])
243245
return data['cycle_id']
244246

@@ -276,13 +278,13 @@ def status(
276278
if not wait_for_complete:
277279
break
278280

279-
if data['internal_stage'] == 'COMMITTING':
280-
pass
281-
elif data['internal_stage'] == 'DONE':
281+
if data['internal_stage'] == 'DONE':
282282
break
283-
elif data['status']['message'] != 'OK':
283+
284+
if data['status']['message'] != 'OK':
284285
break
285286

287+
log.debug(f'data load status:\n{data}')
286288
time.sleep(1)
287289

288290
return data

cs_tools/api/middlewares/user.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,17 @@ def get(
9797
user = user[0]
9898

9999
return user
100+
101+
@validate_arguments
102+
def get_guid(
103+
self,
104+
name: str
105+
) -> Union[GUID, None]:
106+
"""
107+
Returns the GUID for a user or None if the user wasn't found.
108+
:param name: The user name, e.g. somebody@somecompany.com
109+
:return: GUID or None
110+
"""
111+
r = self.ts.api.user.get(name=name)
112+
user = r.json()
113+
return user['header']['id']

cs_tools/api/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .ts_dataservice import TSDataService
22
from .connection import _Connection
33
from .dependency import _Dependency
4+
from .group import Group
45
from .periscope import _Periscope
56
from .metadata import _Metadata, Metadata
67
from .security import _Security, Security

0 commit comments

Comments
 (0)