/
client.py
4009 lines (3280 loc) · 183 KB
/
client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
**************
Synapse Client
**************
The `Synapse` object encapsulates a connection to the Synapse service and is used for building projects, uploading and
retrieving data, and recording provenance of data analysis.
~~~~~
Login
~~~~~
.. automethod:: synapseclient.client.login
~~~~~~~
Synapse
~~~~~~~
.. autoclass:: synapseclient.Synapse
:members:
~~~~~~~~~~~~~~~~
More information
~~~~~~~~~~~~~~~~
See also the `Synapse API documentation <https://docs.synapse.org/rest/>`_.
"""
import collections
import collections.abc
import configparser
import deprecated
import errno
import functools
import getpass
import hashlib
import json
import logging
import mimetypes
import os
import requests
import shutil
import sys
import tempfile
import time
import typing
import urllib.parse as urllib_urlparse
import urllib.request as urllib_request
import warnings
import webbrowser
import zipfile
import synapseclient
from .annotations import (
from_synapse_annotations,
to_synapse_annotations,
Annotations,
convert_old_annotation_json,
check_annotations_changed,
)
from .activity import Activity
import synapseclient.core.multithread_download as multithread_download
from .entity import Entity, File, Folder, Versionable,\
split_entity_namespaces, is_versionable, is_container, is_synapse_entity
from synapseclient.core.models.dict_object import DictObject
from .evaluation import Evaluation, Submission, SubmissionStatus
from .table import Schema, SchemaBase, Column, TableQueryResult, CsvFileTable, EntityViewSchema, SubmissionViewSchema
from .team import UserProfile, Team, TeamMember, UserGroupHeader
from .wiki import Wiki, WikiAttachment
from synapseclient.core import cache, exceptions, utils
from synapseclient.core.constants import config_file_constants
from synapseclient.core.constants import concrete_types
from synapseclient.core import cumulative_transfer_progress
from synapseclient.core.credentials import (
cached_sessions,
delete_stored_credentials,
get_default_credential_chain,
UserLoginArgs,
)
from synapseclient.core.exceptions import (
SynapseAuthenticationError,
SynapseError,
SynapseFileNotFoundError,
SynapseHTTPError,
SynapseMd5MismatchError,
SynapseNoCredentialsError,
SynapseProvenanceError,
SynapseTimeoutError,
SynapseUnmetAccessRestrictions,
)
from synapseclient.core.logging_setup import DEFAULT_LOGGER_NAME, DEBUG_LOGGER_NAME, SILENT_LOGGER_NAME
from synapseclient.core.version_check import version_check
from synapseclient.core.pool_provider import DEFAULT_NUM_THREADS
from synapseclient.core.utils import id_of, get_properties, MB, memoize, is_json, extract_synapse_id_from_query, \
find_data_file_handle, extract_zip_file_to_directory, is_integer, require_param
from synapseclient.core.retry import (
with_retry,
DEFAULT_RETRY_STATUS_CODES,
RETRYABLE_CONNECTION_ERRORS,
RETRYABLE_CONNECTION_EXCEPTIONS,
)
from synapseclient.core import sts_transfer
from synapseclient.core.upload.multipart_upload import multipart_upload_file, multipart_upload_string
from synapseclient.core.remote_file_storage_wrappers import S3ClientWrapper, SFTPWrapper
from synapseclient.core.upload.upload_functions import upload_file_handle, upload_synapse_s3
from synapseclient.core.dozer import doze
PRODUCTION_ENDPOINTS = {'repoEndpoint': 'https://repo-prod.prod.sagebase.org/repo/v1',
'authEndpoint': 'https://auth-prod.prod.sagebase.org/auth/v1',
'fileHandleEndpoint': 'https://file-prod.prod.sagebase.org/file/v1',
'portalEndpoint': 'https://www.synapse.org/'}
STAGING_ENDPOINTS = {'repoEndpoint': 'https://repo-staging.prod.sagebase.org/repo/v1',
'authEndpoint': 'https://auth-staging.prod.sagebase.org/auth/v1',
'fileHandleEndpoint': 'https://file-staging.prod.sagebase.org/file/v1',
'portalEndpoint': 'https://staging.synapse.org/'}
CONFIG_FILE = os.path.join(os.path.expanduser('~'), '.synapseConfig')
SESSION_FILENAME = '.session'
FILE_BUFFER_SIZE = 2*MB
CHUNK_SIZE = 5*MB
QUERY_LIMIT = 1000
CHUNK_UPLOAD_POLL_INTERVAL = 1 # second
ROOT_ENTITY = 'syn4489'
PUBLIC = 273949 # PrincipalId of public "user"
AUTHENTICATED_USERS = 273948
DEBUG_DEFAULT = False
REDIRECT_LIMIT = 5
MAX_THREADS_CAP = 128
# Defines the standard retry policy applied to the rest methods
# The retry period needs to span a minute because sending messages is limited to 10 per 60 seconds.
STANDARD_RETRY_PARAMS = {"retry_status_codes": DEFAULT_RETRY_STATUS_CODES,
"retry_errors": RETRYABLE_CONNECTION_ERRORS,
"retry_exceptions": RETRYABLE_CONNECTION_EXCEPTIONS,
"retries": 60, # Retries for up to about 30 minutes
"wait": 1,
"max_wait": 30,
"back_off": 2}
# Add additional mimetypes
mimetypes.add_type('text/x-r', '.R', strict=False)
mimetypes.add_type('text/x-r', '.r', strict=False)
mimetypes.add_type('text/tab-separated-values', '.maf', strict=False)
mimetypes.add_type('text/tab-separated-values', '.bed5', strict=False)
mimetypes.add_type('text/tab-separated-values', '.bed', strict=False)
mimetypes.add_type('text/tab-separated-values', '.vcf', strict=False)
mimetypes.add_type('text/tab-separated-values', '.sam', strict=False)
mimetypes.add_type('text/yaml', '.yaml', strict=False)
mimetypes.add_type('text/x-markdown', '.md', strict=False)
mimetypes.add_type('text/x-markdown', '.markdown', strict=False)
DEFAULT_STORAGE_LOCATION_ID = 1
def login(*args, **kwargs):
"""
Convenience method to create a Synapse object and login.
See :py:func:`synapseclient.Synapse.login` for arguments and usage.
Example::
import synapseclient
syn = synapseclient.login()
"""
syn = Synapse()
syn.login(*args, **kwargs)
return syn
class Synapse(object):
"""
Constructs a Python client object for the Synapse repository service
:param repoEndpoint: Location of Synapse repository
:param authEndpoint: Location of authentication service
:param fileHandleEndpoint: Location of file service
:param portalEndpoint: Location of the website
:param serviceTimeoutSeconds: Wait time before timeout (currently unused)
:param debug: Print debugging messages if True
:param skip_checks: Skip version and endpoint checks
:param configPath: Path to config File with setting for Synapse
defaults to ~/.synapseConfig
:param requests_session a custom requests.Session object that this Synapse instance will use
when making http requests
Typically, no parameters are needed::
import synapseclient
syn = synapseclient.Synapse()
See:
- :py:func:`synapseclient.Synapse.login`
- :py:func:`synapseclient.Synapse.setEndpoints`
"""
# TODO: add additional boolean for write to disk?
def __init__(self, repoEndpoint=None, authEndpoint=None, fileHandleEndpoint=None, portalEndpoint=None,
debug=None, skip_checks=False, configPath=CONFIG_FILE, requests_session=None,
cache_root_dir=None, silent=None):
self._requests_session = requests_session or requests.Session()
cache_root_dir = cache.CACHE_ROOT_DIR if cache_root_dir is None else cache_root_dir
config_debug = None
# Check for a config file
self.configPath = configPath
if os.path.isfile(configPath):
config = self.getConfigFile(configPath)
if config.has_option('cache', 'location'):
cache_root_dir = config.get('cache', 'location')
if config.has_section('debug'):
config_debug = True
if debug is None:
debug = config_debug if config_debug is not None else DEBUG_DEFAULT
self.cache = cache.Cache(cache_root_dir)
self._sts_token_store = sts_transfer.StsTokenStore()
self.setEndpoints(repoEndpoint, authEndpoint, fileHandleEndpoint, portalEndpoint, skip_checks)
self.default_headers = {'content-type': 'application/json; charset=UTF-8',
'Accept': 'application/json; charset=UTF-8'}
self.credentials = None
if not isinstance(debug, bool):
raise ValueError("debug must be set to a bool (either True or False)")
self.debug = debug
self.silent = silent
self._init_logger() # initializes self.logger
self.skip_checks = skip_checks
self.table_query_sleep = 2
self.table_query_backoff = 1.1
self.table_query_max_sleep = 20
self.table_query_timeout = 600 # in seconds
self.multi_threaded = True # if set to True, multi threaded download will be used for http and https URLs
transfer_config = self._get_transfer_config()
self.max_threads = transfer_config['max_threads']
self.use_boto_sts_transfers = transfer_config['use_boto_sts']
# initialize logging
def _init_logger(self):
logger_name = SILENT_LOGGER_NAME if self.silent else DEBUG_LOGGER_NAME if self.debug else DEFAULT_LOGGER_NAME
self.logger = logging.getLogger(logger_name)
logging.getLogger('py.warnings').handlers = self.logger.handlers
@property
def max_threads(self):
return self._max_threads
@max_threads.setter
def max_threads(self, value: int):
self._max_threads = min(max(value, 1), MAX_THREADS_CAP)
@property
def username(self):
# for backwards compatability when username was a part of the Synapse object and not in credentials
return self.credentials.username if self.credentials is not None else None
@functools.lru_cache()
def getConfigFile(self, configPath):
"""
Retrieves the client configuration information.
:param configPath: Path to configuration file on local file system
:return: a RawConfigParser populated with properties from the user's configuration file.
"""
try:
config = configparser.RawConfigParser()
config.read(configPath) # Does not fail if the file does not exist
return config
except configparser.Error as ex:
raise ValueError("Error parsing Synapse config file: {}".format(configPath)) from ex
def setEndpoints(self, repoEndpoint=None, authEndpoint=None, fileHandleEndpoint=None, portalEndpoint=None,
skip_checks=False):
"""
Sets the locations for each of the Synapse services (mostly useful for testing).
:param repoEndpoint: Location of synapse repository
:param authEndpoint: Location of authentication service
:param fileHandleEndpoint: Location of file service
:param portalEndpoint: Location of the website
:param skip_checks: Skip version and endpoint checks
To switch between staging and production endpoints::
syn.setEndpoints(**synapseclient.client.STAGING_ENDPOINTS)
syn.setEndpoints(**synapseclient.client.PRODUCTION_ENDPOINTS)
"""
endpoints = {'repoEndpoint': repoEndpoint,
'authEndpoint': authEndpoint,
'fileHandleEndpoint': fileHandleEndpoint,
'portalEndpoint': portalEndpoint}
# For unspecified endpoints, first look in the config file
config = self.getConfigFile(self.configPath)
for point in endpoints.keys():
if endpoints[point] is None and config.has_option('endpoints', point):
endpoints[point] = config.get('endpoints', point)
# Endpoints default to production
for point in endpoints.keys():
if endpoints[point] is None:
endpoints[point] = PRODUCTION_ENDPOINTS[point]
# Update endpoints if we get redirected
if not skip_checks:
response = self._requests_session.get(endpoints[point], allow_redirects=False,
headers=synapseclient.USER_AGENT)
if response.status_code == 301:
endpoints[point] = response.headers['location']
self.repoEndpoint = endpoints['repoEndpoint']
self.authEndpoint = endpoints['authEndpoint']
self.fileHandleEndpoint = endpoints['fileHandleEndpoint']
self.portalEndpoint = endpoints['portalEndpoint']
def login(self, email=None, password=None, apiKey=None, sessionToken=None, rememberMe=False, silent=False,
forced=False, authToken=None):
"""
Valid combinations of login() arguments:
- email/username and password
- email/username and apiKey (Base64 encoded string)
- authToken
- sessionToken (**DEPRECATED**)
If no login arguments are provided or only username is provided, login() will attempt to log in using
information from these sources (in order of preference):
#. User's personal access token from environment the variable: SYNAPSE_AUTH_TOKEN
#. .synapseConfig file (in user home folder unless configured otherwise)
#. cached credentials from previous `login()` where `rememberMe=True` was passed as a parameter
:param email: Synapse user name (or an email address associated with a Synapse account)
:param password: password
:param apiKey: Base64 encoded Synapse API key
:param sessionToken: **!!DEPRECATED FIELD!!** User's current session token. Using this field will ignore the
following fields: email, password, apiKey
:param rememberMe: Whether the authentication information should be cached in your operating system's
credential storage.
:param authToken: A bearer authorization token, e.g. a personal access token, can be used in lieu of a
password or apiKey
**GNOME Keyring** (recommended) or **KWallet** is recommended to be installed for credential storage on
**Linux** systems.
If it is not installed/setup, credentials will be stored as PLAIN-TEXT file with read and write permissions for
the current user only (chmod 600).
On Windows and Mac OS, a default credentials storage exists so it will be preferred over the plain-text file.
To install GNOME Keyring on Ubuntu::
sudo apt-get install gnome-keyring
sudo apt-get install python-dbus #(for Python 2 installed via apt-get)
OR
sudo apt-get install python3-dbus #(for Python 3 installed via apt-get)
OR
sudo apt-get install libdbus-glib-1-dev #(for custom installation of Python or vitualenv)
sudo pip install dbus-python #(may take a while to compile C code)
If you are on a headless Linux session (e.g. connecting via SSH), please run the following commands before
running your Python session::
dbus-run-session -- bash #(replace 'bash' with 'sh' if bash is unavailable)
echo -n "REPLACE_WITH_YOUR_KEYRING_PASSWORD"|gnome-keyring-daemon -- unlock
:param silent: Defaults to False. Suppresses the "Welcome ...!" message.
:param forced: Defaults to False. Bypass the credential cache if set.
Example::
syn.login('my-username', 'secret-password', rememberMe=True)
#> Welcome, Me!
After logging in with the *rememberMe* flag set, an API key will be cached and
used to authenticate for future logins::
syn.login()
#> Welcome, Me!
"""
# Note: the order of the logic below reflects the ordering in the docstring above.
# Check version before logging in
if not self.skip_checks:
version_check()
# Make sure to invalidate the existing session
self.logout()
credential_provider_chain = get_default_credential_chain()
# TODO: remove deprecated sessionToken when we move to a different solution
self.credentials = credential_provider_chain.get_credentials(
self,
UserLoginArgs(
email,
password,
apiKey,
forced,
sessionToken,
authToken,
)
)
# Final check on login success
if not self.credentials:
raise SynapseNoCredentialsError("No credentials provided.")
# Save the API key in the cache
if rememberMe:
delete_stored_credentials(self.credentials.username)
self.credentials.store_to_keyring()
cached_sessions.set_most_recent_user(self.credentials.username)
if not silent:
profile = self.getUserProfile(refresh=True)
# TODO-PY3: in Python2, do we need to ensure that this is encoded in utf-8
self.logger.info("Welcome, %s!\n" % (profile['displayName'] if 'displayName' in profile
else self.credentials.username))
def _get_config_section_dict(self, section_name):
config = self.getConfigFile(self.configPath)
try:
return dict(config.items(section_name))
except configparser.NoSectionError:
# section not present
return {}
def _get_config_authentication(self):
return self._get_config_section_dict(config_file_constants.AUTHENTICATION_SECTION_NAME)
def _get_client_authenticated_s3_profile(self, endpoint, bucket):
config_section = endpoint + "/" + bucket
return self._get_config_section_dict(config_section).get("profile_name", "default")
def _get_transfer_config(self):
# defaults
transfer_config = {
'max_threads': DEFAULT_NUM_THREADS,
'use_boto_sts': False
}
for k, v in self._get_config_section_dict('transfer').items():
if v:
if k == 'max_threads' and v:
try:
transfer_config['max_threads'] = int(v)
except ValueError as cause:
raise ValueError(f"Invalid transfer.max_threads config setting {v}") from cause
elif k == 'use_boto_sts':
lower_v = v.lower()
if lower_v not in ('true', 'false'):
raise ValueError(f"Invalid transfer.use_boto_sts config setting {v}")
transfer_config['use_boto_sts'] = 'true' == lower_v
return transfer_config
def _getSessionToken(self, email, password):
"""Returns a validated session token."""
try:
req = {'email': email, 'password': password}
session = self.restPOST('/session', body=json.dumps(req), endpoint=self.authEndpoint,
headers=self.default_headers)
return session['sessionToken']
except SynapseHTTPError as err:
if err.response.status_code == 403 or err.response.status_code == 404 or err.response.status_code == 401:
raise SynapseAuthenticationError("Invalid username or password.")
raise
def _getAPIKey(self, sessionToken):
"""Uses a session token to fetch an API key."""
headers = {'sessionToken': sessionToken, 'Accept': 'application/json'}
secret = self.restGET('/secretKey', endpoint=self.authEndpoint, headers=headers)
return secret['secretKey']
def _loggedIn(self):
"""Test whether the user is logged in to Synapse."""
if self.credentials is None:
return False
try:
user = self.restGET('/userProfile')
if 'displayName' in user:
if user['displayName'] == 'Anonymous':
return False
return user['displayName']
elif 'userName' in user:
return user['userName']
except SynapseHTTPError as err:
if err.response.status_code == 401:
return False
raise
def logout(self, forgetMe=False):
"""
Removes authentication information from the Synapse client.
:param forgetMe: Set as True to clear any local storage of authentication information.
See the flag "rememberMe" in :py:func:`synapseclient.Synapse.login`.
"""
# Delete the user's API key from the cache
if forgetMe and self.credentials:
self.credentials.delete_from_keyring()
self.credentials = None
def invalidateAPIKey(self):
"""Invalidates authentication across all clients."""
# Logout globally
if self._loggedIn():
self.restDELETE('/secretKey', endpoint=self.authEndpoint)
@memoize
def getUserProfile(self, id=None, sessionToken=None, refresh=False):
"""
Get the details about a Synapse user.
Retrieves information on the current user if 'id' is omitted.
:param id: The 'userId' (aka 'ownerId') of a user or the userName
:param sessionToken: The session token to use to find the user profile
:param refresh: If set to True will always fetch the data from Synape otherwise will use cached information
:returns: The user profile for the user of interest.
Example::
my_profile = syn.getUserProfile()
freds_profile = syn.getUserProfile('fredcommo')
"""
try:
# if id is unset or a userID, this will succeed
id = '' if id is None else int(id)
except (TypeError, ValueError):
if isinstance(id, collections.abc.Mapping) and 'ownerId' in id:
id = id.ownerId
elif isinstance(id, TeamMember):
id = id.member.ownerId
else:
principals = self._findPrincipals(id)
if len(principals) == 1:
id = principals[0]['ownerId']
else:
for principal in principals:
if principal.get('userName', None).lower() == id.lower():
id = principal['ownerId']
break
else: # no break
raise ValueError('Can\'t find user "%s": ' % id)
uri = '/userProfile/%s' % id
return UserProfile(**self.restGET(uri, headers={'sessionToken': sessionToken} if sessionToken else None))
def _findPrincipals(self, query_string):
"""
Find users or groups by name or email.
:returns: A list of userGroupHeader objects with fields displayName, email, firstName, lastName, isIndividual,
ownerId
Example::
syn._findPrincipals('test')
[{u'displayName': u'Synapse Test',
u'email': u'syn...t@sagebase.org',
u'firstName': u'Synapse',
u'isIndividual': True,
u'lastName': u'Test',
u'ownerId': u'1560002'},
{u'displayName': ... }]
"""
uri = '/userGroupHeaders?prefix=%s' % urllib_urlparse.quote(query_string)
return [UserGroupHeader(**result) for result in self._GET_paginated(uri)]
def _get_certified_passing_record(self, userid: int) -> dict:
"""Retrieve the Passing Record on the User Certification test for the given user.
:params userid: Synapse user Id
:returns: Synapse Passing Record
https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/quiz/PassingRecord.html
"""
response = self.restGET(f"/user/{userid}/certifiedUserPassingRecord")
return response
def is_certified(self, user: typing.Union[str, int]) -> bool:
"""Determines whether a Synapse user is a certified user.
:params user: Synapse username or Id
:returns: True if the Synapse user is certified
"""
# Check if userid or username exists
syn_user = self.getUserProfile(user)
# Get passing record
try:
certification_status = self._get_certified_passing_record(syn_user['ownerId'])
return certification_status['passed']
except SynapseHTTPError as ex:
if ex.response.status_code == 404:
# user hasn't taken the quiz
return False
raise
def onweb(self, entity, subpageId=None):
"""Opens up a browser window to the entity page or wiki-subpage.
:param entity: Either an Entity or a Synapse ID
:param subpageId: (Optional) ID of one of the wiki's sub-pages
"""
if isinstance(entity, str) and os.path.isfile(entity):
entity = self.get(entity, downloadFile=False)
synId = id_of(entity)
if subpageId is None:
webbrowser.open("%s#!Synapse:%s" % (self.portalEndpoint, synId))
else:
webbrowser.open("%s#!Wiki:%s/ENTITY/%s" % (self.portalEndpoint, synId, subpageId))
def printEntity(self, entity, ensure_ascii=True):
"""
Pretty prints an Entity.
:param entity: The entity to be printed.
:param ensure_ascii: If True, escapes all non-ASCII characters
"""
if utils.is_synapse_id(entity):
entity = self._getEntity(entity)
try:
self.logger.info(json.dumps(entity, sort_keys=True, indent=2, ensure_ascii=ensure_ascii))
except TypeError:
self.logger.info(str(entity))
def _print_transfer_progress(self, *args, **kwargs):
# Checking synapse if the mode is silent mode.
# If self.silent is True, no need to print out transfer progress.
if self.silent is not True:
cumulative_transfer_progress.printTransferProgress(*args, **kwargs)
############################################################
# Service methods #
############################################################
_services = {
"json_schema": "JsonSchemaService",
}
def get_available_services(self):
services = self._services.keys()
return list(services)
def service(self, service_name: str):
import synapseclient.services
assert isinstance(service_name, str)
service_name = service_name.lower().replace(" ", "_")
assert service_name in self._services, (
f"Unrecognized service ({service_name}). Run the 'get_available_"
"services()' method to get a list of available services."
)
service_attr = self._services[service_name]
service_cls = getattr(synapseclient.services, service_attr)
service = service_cls(self)
return service
############################################################
# Get / Store methods #
############################################################
def get(self, entity, **kwargs):
"""
Gets a Synapse entity from the repository service.
:param entity: A Synapse ID, a Synapse Entity object, a plain dictionary in which 'id' maps to a
Synapse ID or a local file that is stored in Synapse (found by the file MD5)
:param version: The specific version to get.
Defaults to the most recent version.
:param downloadFile: Whether associated files(s) should be downloaded.
Defaults to True
:param downloadLocation: Directory where to download the Synapse File Entity.
Defaults to the local cache.
:param followLink: Whether the link returns the target Entity.
Defaults to False
:param ifcollision: Determines how to handle file collisions.
May be "overwrite.local", "keep.local", or "keep.both".
Defaults to "keep.both".
:param limitSearch: a Synanpse ID used to limit the search in Synapse if entity is specified as a local
file. That is, if the file is stored in multiple locations in Synapse only the ones
in the specified folder/project will be returned.
:returns: A new Synapse Entity object of the appropriate type
Example::
# download file into cache
entity = syn.get('syn1906479')
print(entity.name)
print(entity.path)
# download file into current working directory
entity = syn.get('syn1906479', downloadLocation='.')
print(entity.name)
print(entity.path)
# Determine the provenance of a locally stored file as indicated in Synapse
entity = syn.get('/path/to/file.txt', limitSearch='syn12312')
print(syn.getProvenance(entity))
"""
# If entity is a local file determine the corresponding synapse entity
if isinstance(entity, str) and os.path.isfile(entity):
bundle = self._getFromFile(entity, kwargs.pop('limitSearch', None))
kwargs['downloadFile'] = False
kwargs['path'] = entity
elif isinstance(entity, str) and not utils.is_synapse_id(entity):
raise SynapseFileNotFoundError(
('The parameter %s is neither a local file path '
' or a valid entity id' % entity)
)
# have not been saved entities
elif isinstance(entity, Entity) and not entity.get('id'):
raise ValueError(
"Cannot retrieve entity that has not been saved."
" Please use syn.store() to save your entity and try again."
)
else:
version = kwargs.get('version', None)
bundle = self._getEntityBundle(entity, version)
# Check and warn for unmet access requirements
self._check_entity_restrictions(bundle, entity, kwargs.get('downloadFile', True))
return self._getWithEntityBundle(entityBundle=bundle, entity=entity, **kwargs)
def _check_entity_restrictions(self, bundle, entity, downloadFile):
restrictionInformation = bundle['restrictionInformation']
if restrictionInformation['hasUnmetAccessRequirement']:
warning_message = ("\nThis entity has access restrictions. Please visit the web page for this entity "
"(syn.onweb(\"%s\")). Click the downward pointing arrow next to the file's name to "
"review and fulfill its download requirement(s).\n" % id_of(entity))
if downloadFile and bundle.get('entityType') not in ('project', 'folder'):
raise SynapseUnmetAccessRestrictions(warning_message)
warnings.warn(warning_message)
def _getFromFile(self, filepath, limitSearch=None):
"""
Gets a Synapse entityBundle based on the md5 of a local file
See :py:func:`synapseclient.Synapse.get`.
:param filepath: path to local file
:param limitSearch: Limits the places in Synapse where the file is searched for.
"""
results = self.restGET('/entity/md5/%s' % utils.md5_for_file(filepath).hexdigest())['results']
if limitSearch is not None:
# Go through and find the path of every entity found
paths = [self.restGET('/entity/%s/path' % ent['id']) for ent in results]
# Filter out all entities whose path does not contain limitSearch
results = [ent for ent, path in zip(results, paths) if
utils.is_in_path(limitSearch, path)]
if len(results) == 0: # None found
raise SynapseFileNotFoundError('File %s not found in Synapse' % (filepath,))
elif len(results) > 1:
id_txts = '\n'.join(['%s.%i' % (r['id'], r['versionNumber']) for r in results])
self.logger.warning('\nThe file %s is associated with many files in Synapse:\n%s\n'
'You can limit to files in specific project or folder by setting the limitSearch to the'
' synapse Id of the project or folder.\n'
'Will use the first one returned: \n'
'%s version %i\n' % (filepath, id_txts, results[0]['id'], results[0]['versionNumber']))
entity = results[0]
bundle = self._getEntityBundle(entity, version=entity['versionNumber'])
self.cache.add(bundle['entity']['dataFileHandleId'], filepath)
return bundle
def move(self, entity, new_parent):
"""
Move a Synapse entity to a new container.
:param entity: A Synapse ID, a Synapse Entity object, or a local file that is stored in Synapse
:param new_parent: The new parent container (Folder or Project) to which the entity should be moved.
:returns: The Synapse Entity object that has been moved.
Example::
entity = syn.move('syn456', 'syn123')
"""
entity = self.get(entity, downloadFile=False)
entity.parentId = id_of(new_parent)
entity = self.store(entity, forceVersion=False)
return entity
def _getWithEntityBundle(self, entityBundle, entity=None, **kwargs):
"""
Creates a :py:mod:`synapseclient.Entity` from an entity bundle returned by Synapse.
An existing Entity can be supplied in case we want to refresh a stale Entity.
:param entityBundle: Uses the given dictionary as the meta information of the Entity to get
:param entity: Optional, entity whose local state will be copied into the returned entity
:param submission: Optional, access associated files through a submission rather than
through an entity.
See :py:func:`synapseclient.Synapse.get`.
See :py:func:`synapseclient.Synapse._getEntityBundle`.
See :py:mod:`synapseclient.Entity`.
"""
# Note: This version overrides the version of 'entity' (if the object is Mappable)
kwargs.pop('version', None)
downloadFile = kwargs.pop('downloadFile', True)
downloadLocation = kwargs.pop('downloadLocation', None)
ifcollision = kwargs.pop('ifcollision', None)
submission = kwargs.pop('submission', None)
followLink = kwargs.pop('followLink', False)
path = kwargs.pop('path', None)
# make sure user didn't accidentlaly pass a kwarg that we don't handle
if kwargs: # if there are remaining items in the kwargs
raise TypeError('Unexpected **kwargs: %r' % kwargs)
# If Link, get target ID entity bundle
if entityBundle['entity']['concreteType'] == 'org.sagebionetworks.repo.model.Link' and followLink:
targetId = entityBundle['entity']['linksTo']['targetId']
targetVersion = entityBundle['entity']['linksTo'].get('targetVersionNumber')
entityBundle = self._getEntityBundle(targetId, targetVersion)
# TODO is it an error to specify both downloadFile=False and downloadLocation?
# TODO this matters if we want to return already cached files when downloadFile=False
# Make a fresh copy of the Entity
local_state = entity.local_state() if entity and isinstance(entity, Entity) else {}
if path is not None:
local_state['path'] = path
properties = entityBundle['entity']
annotations = from_synapse_annotations(entityBundle['annotations'])
entity = Entity.create(properties, annotations, local_state)
# Handle download of fileEntities
if isinstance(entity, File):
# update the entity with FileHandle metadata
file_handle = next((handle for handle in entityBundle['fileHandles']
if handle['id'] == entity.dataFileHandleId), None)
entity._update_file_handle(file_handle)
if downloadFile:
if file_handle:
self._download_file_entity(downloadLocation, entity, ifcollision, submission)
else: # no filehandle means that we do not have DOWNLOAD permission
warning_message = "WARNING: You have READ permission on this file entity but not DOWNLOAD " \
"permission. The file has NOT been downloaded."
self.logger.warning('\n' + '!'*len(warning_message)+'\n' + warning_message + '\n'
+ '!'*len(warning_message)+'\n')
return entity
def _ensure_download_location_is_directory(self, downloadLocation):
download_dir = os.path.expandvars(os.path.expanduser(downloadLocation))
if os.path.isfile(download_dir):
raise ValueError("Parameter 'downloadLocation' should be a directory, not a file.")
return download_dir
def _download_file_entity(self, downloadLocation, entity, ifcollision, submission):
# set the initial local state
entity.path = None
entity.files = []
entity.cacheDir = None
# check to see if an UNMODIFIED version of the file (since it was last downloaded) already exists
# this location could be either in .synapseCache or a user specified location to which the user previously
# downloaded the file
cached_file_path = self.cache.get(entity.dataFileHandleId, downloadLocation)
# location in .synapseCache where the file would be corresponding to its FileHandleId
synapseCache_location = self.cache.get_cache_dir(entity.dataFileHandleId)
file_name = entity._file_handle.fileName if cached_file_path is None else os.path.basename(cached_file_path)
# Decide the best download location for the file
if downloadLocation is not None:
# Make sure the specified download location is a fully resolved directory
downloadLocation = self._ensure_download_location_is_directory(downloadLocation)
elif cached_file_path is not None:
# file already cached so use that as the download location
downloadLocation = os.path.dirname(cached_file_path)
else:
# file not cached and no user-specified location so default to .synapseCache
downloadLocation = synapseCache_location
# resolve file path collisions by either overwriting, renaming, or not downloading, depending on the
# ifcollision value
downloadPath = self._resolve_download_path_collisions(downloadLocation, file_name, ifcollision,
synapseCache_location, cached_file_path)
if downloadPath is None:
return
if cached_file_path is not None: # copy from cache
if downloadPath != cached_file_path:
# create the foider if it does not exist already
if not os.path.exists(downloadLocation):
os.makedirs(downloadLocation)
shutil.copy(cached_file_path, downloadPath)
else: # download the file from URL (could be a local file)
objectType = 'FileEntity' if submission is None else 'SubmissionAttachment'
objectId = entity['id'] if submission is None else submission
# reassign downloadPath because if url points to local file (e.g. file://~/someLocalFile.txt)
# it won't be "downloaded" and, instead, downloadPath will just point to '~/someLocalFile.txt'
# _downloadFileHandle may also return None to indicate that the download failed
downloadPath = self._downloadFileHandle(entity.dataFileHandleId, objectId, objectType, downloadPath)
if downloadPath is None or not os.path.exists(downloadPath):
return
# converts the path format from forward slashes back to backward slashes on Windows
entity.path = os.path.normpath(downloadPath)
entity.files = [os.path.basename(downloadPath)]
entity.cacheDir = os.path.dirname(downloadPath)
def _resolve_download_path_collisions(self, downloadLocation, file_name, ifcollision, synapseCache_location,
cached_file_path):
# always overwrite if we are downloading to .synapseCache
if utils.normalize_path(downloadLocation) == synapseCache_location:
if ifcollision is not None:
self.logger.warning('\n' + '!'*50+'\nifcollision=' + ifcollision
+ 'is being IGNORED because the download destination is synapse\'s cache.'
' Instead, the behavior is "overwrite.local". \n'+'!'*50+'\n')
ifcollision = 'overwrite.local'
# if ifcollision not specified, keep.local
ifcollision = ifcollision or 'keep.both'
downloadPath = utils.normalize_path(os.path.join(downloadLocation, file_name))
# resolve collison
if os.path.exists(downloadPath):
if ifcollision == "overwrite.local":
pass
elif ifcollision == "keep.local":
# Don't want to overwrite the local file.
return None
elif ifcollision == "keep.both":
if downloadPath != cached_file_path:
return utils.unique_filename(downloadPath)
else:
raise ValueError('Invalid parameter: "%s" is not a valid value '
'for "ifcollision"' % ifcollision)
return downloadPath
def store(self, obj, *, createOrUpdate=True, forceVersion=True, versionLabel=None, isRestricted=False,
activity=None, used=None, executed=None, activityName=None, activityDescription=None):
"""
Creates a new Entity or updates an existing Entity, uploading any files in the process.
:param obj: A Synapse Entity, Evaluation, or Wiki
:param used: The Entity, Synapse ID, or URL used to create the object (can also be a list of
these)
:param executed: The Entity, Synapse ID, or URL representing code executed to create the object
(can also be a list of these)
:param activity: Activity object specifying the user's provenance.
:param activityName: Activity name to be used in conjunction with *used* and *executed*.
:param activityDescription: Activity description to be used in conjunction with *used* and *executed*.
:param createOrUpdate: Indicates whether the method should automatically perform an update if the 'obj'
conflicts with an existing Synapse object. Defaults to True.
:param forceVersion: Indicates whether the method should increment the version of the object even if
nothing has changed. Defaults to True.
:param versionLabel: Arbitrary string used to label the version.
:param isRestricted: If set to true, an email will be sent to the Synapse access control team to start
the process of adding terms-of-use or review board approval for this entity.
You will be contacted with regards to the specific data being restricted and the
requirements of access.
:returns: A Synapse Entity, Evaluation, or Wiki
Example::
from synapseclient import Project