dcicutils 7.4.1__tar.gz → 7.4.2.1b0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dcicutils might be problematic. Click here for more details.

Files changed (51) hide show
  1. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/PKG-INFO +1 -1
  2. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/common.py +4 -1
  3. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/glacier_utils.py +36 -4
  4. dcicutils-7.4.2.1b0/dcicutils/project_utils.py +257 -0
  5. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/s3_utils.py +4 -0
  6. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/pyproject.toml +1 -1
  7. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/setup.py +1 -1
  8. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/LICENSE.txt +0 -0
  9. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/README.rst +0 -0
  10. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/__init__.py +0 -0
  11. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/base.py +0 -0
  12. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/beanstalk_utils.py +0 -0
  13. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/cloudformation_utils.py +0 -0
  14. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/codebuild_utils.py +0 -0
  15. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/command_utils.py +0 -0
  16. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/creds_utils.py +0 -0
  17. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/data_utils.py +0 -0
  18. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/deployment_utils.py +0 -0
  19. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/diff_utils.py +0 -0
  20. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/docker_utils.py +0 -0
  21. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/ecr_scripts.py +0 -0
  22. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/ecr_utils.py +0 -0
  23. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/ecs_utils.py +0 -0
  24. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/env_base.py +0 -0
  25. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/env_manager.py +0 -0
  26. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/env_scripts.py +0 -0
  27. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/env_utils.py +0 -0
  28. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/env_utils_legacy.py +0 -0
  29. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/es_utils.py +0 -0
  30. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/exceptions.py +0 -0
  31. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/ff_mocks.py +0 -0
  32. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/ff_utils.py +0 -0
  33. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/function_cache_decorator.py +0 -0
  34. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/jh_utils.py +0 -0
  35. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/kibana/dashboards.json +0 -0
  36. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/kibana/readme.md +0 -0
  37. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/lang_utils.py +0 -0
  38. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/log_utils.py +0 -0
  39. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/misc_utils.py +0 -0
  40. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/obfuscation_utils.py +0 -0
  41. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/opensearch_utils.py +0 -0
  42. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/qa_checkers.py +0 -0
  43. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/qa_utils.py +0 -0
  44. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/redis_tools.py +0 -0
  45. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/redis_utils.py +0 -0
  46. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/scripts/publish_to_pypi.py +0 -0
  47. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/secrets_utils.py +0 -0
  48. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/snapshot_utils.py +0 -0
  49. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/ssl_certificate_utils.py +0 -0
  50. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/task_utils.py +0 -0
  51. {dcicutils-7.4.1 → dcicutils-7.4.2.1b0}/dcicutils/trace_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcicutils
3
- Version: 7.4.1
3
+ Version: 7.4.2.1b0
4
4
  Summary: Utility package for interacting with the 4DN Data Portal and other 4DN resources
5
5
  Home-page: https://github.com/4dn-dcic/utils
6
6
  License: MIT
@@ -94,7 +94,6 @@ AVAILABLE_S3_STORAGE_CLASSES = [
94
94
  #
95
95
  # See boto3 docs for info on possible values, but these 3 are the current ones used for
96
96
  # glacier (that require restore calls) - Will 7 Apr 2023
97
-
98
97
  S3_GLACIER_CLASSES = [
99
98
  'GLACIER_IR', # Glacier Instant Retrieval
100
99
  'GLACIER', # Glacier Flexible Retrieval
@@ -118,6 +117,10 @@ S3StorageClass = Union[
118
117
  ]
119
118
 
120
119
 
120
+ # This constant is used in our Lifecycle management system to automatically transition objects
121
+ ENCODED_LIFECYCLE_TAG_KEY = 'Lifecycle'
122
+
123
+
121
124
  # These numbers come from AWS and is the max size that can be copied with a single request
122
125
  # Any larger than this requires a multipart upload - Will 24 April 2023
123
126
  MAX_STANDARD_COPY_SIZE = 5368709120
@@ -2,7 +2,10 @@ import boto3
2
2
  from typing import Union, List, Tuple
3
3
  from concurrent.futures import ThreadPoolExecutor
4
4
  from tqdm import tqdm
5
- from .common import S3_GLACIER_CLASSES, S3StorageClass, MAX_MULTIPART_CHUNKS, MAX_STANDARD_COPY_SIZE
5
+ from .common import (
6
+ S3_GLACIER_CLASSES, S3StorageClass, MAX_MULTIPART_CHUNKS, MAX_STANDARD_COPY_SIZE,
7
+ ENCODED_LIFECYCLE_TAG_KEY
8
+ )
6
9
  from .command_utils import require_confirmation
7
10
  from .misc_utils import PRINT
8
11
  from .ff_utils import get_metadata, search_metadata, get_health_page, patch_metadata
@@ -255,8 +258,19 @@ class GlacierUtils:
255
258
  PRINT(f'Error deleting Glacier versions of object {bucket}/{key}: {str(e)}')
256
259
  return False
257
260
 
261
+ @staticmethod
262
+ def _format_tags(tags: List[dict]) -> str:
263
+ """ Helper method that formats tags so that they match the format expected by the boto3 API
264
+
265
+ :param tags: array of dictionaries containing Key, Value mappings to be reformatted
266
+ :return: String formatted tag list ie:
267
+ [{Key: key1, Value: value1}, Key: key2, Value: value2}] --> 'key1=value1&key2=value2'
268
+ """
269
+ return '&'.join([f'{tag["Key"]}={tag["Value"]}' for tag in tags])
270
+
258
271
  def _do_multipart_upload(self, bucket: str, key: str, total_size: int, part_size: int = 200,
259
- storage_class: str = 'STANDARD', version_id: Union[str, None] = None) -> Union[dict, None]:
272
+ storage_class: str = 'STANDARD', tags: str = '',
273
+ version_id: Union[str, None] = None) -> Union[dict, None]:
260
274
  """ Helper function for copy_object_back_to_original_location, not intended to
261
275
  be called directly, will arrange for a multipart copy of large updates
262
276
  to change storage class
@@ -266,6 +280,7 @@ class GlacierUtils:
266
280
  :param total_size: total size of object
267
281
  :param part_size: what size to divide the object into when uploading the chunks
268
282
  :param storage_class: new storage class to use
283
+ :param tags: string of tags to apply
269
284
  :param version_id: object version ID, if applicable
270
285
  :return: response, if successful, or else None
271
286
  """
@@ -275,7 +290,12 @@ class GlacierUtils:
275
290
  if num_parts > MAX_MULTIPART_CHUNKS:
276
291
  raise GlacierRestoreException(f'Must user a part_size larger than {part_size}'
277
292
  f' that will result in fewer than {MAX_MULTIPART_CHUNKS} chunks')
278
- mpu = self.s3.create_multipart_upload(Bucket=bucket, Key=key, StorageClass=storage_class)
293
+ cmu = {
294
+ 'Bucket': bucket, 'Key': key, 'StorageClass': storage_class
295
+ }
296
+ if tags:
297
+ cmu['Tagging'] = tags
298
+ mpu = self.s3.create_multipart_upload(**cmu)
279
299
  mpu_upload_id = mpu['UploadId']
280
300
  except Exception as e:
281
301
  PRINT(f'Error creating multipart upload for {bucket}/{key} : {str(e)}')
@@ -327,6 +347,7 @@ class GlacierUtils:
327
347
 
328
348
  def copy_object_back_to_original_location(self, bucket: str, key: str, storage_class: str = 'STANDARD',
329
349
  part_size: int = 200, # MB
350
+ preserve_lifecycle_tag: bool = False,
330
351
  version_id: Union[str, None] = None) -> Union[dict, None]:
331
352
  """ Reads the temporary location from the restored object and copies it back to the original location
332
353
 
@@ -334,6 +355,7 @@ class GlacierUtils:
334
355
  :param key: key within bucket where object is stored
335
356
  :param storage_class: new storage class for this object
336
357
  :param part_size: if doing a large copy, size of chunks to upload (in MB)
358
+ :param preserve_lifecycle_tag: whether to keep existing lifecycle tag on the object
337
359
  :param version_id: version of object, if applicable
338
360
  :return: boolean whether the copy was successful
339
361
  """
@@ -342,12 +364,20 @@ class GlacierUtils:
342
364
  response = self.s3.head_object(Bucket=bucket, Key=key)
343
365
  size = response['ContentLength']
344
366
  multipart = (size >= MAX_STANDARD_COPY_SIZE)
367
+ if not preserve_lifecycle_tag: # default: preserve tags except 'Lifecycle'
368
+ tags = self.s3.get_object_tagging(Bucket=bucket, Key=key).get('TagSet', [])
369
+ tags = [tag for tag in tags if tag['Key'] != ENCODED_LIFECYCLE_TAG_KEY]
370
+ tags = self._format_tags(tags)
371
+ if not tags:
372
+ self.s3.delete_object_tagging(Bucket=bucket, Key=key)
373
+ else:
374
+ tags = ''
345
375
  except Exception as e:
346
376
  PRINT(f'Could not retrieve metadata on file {bucket}/{key} : {str(e)}')
347
377
  return None
348
378
  try:
349
379
  if multipart:
350
- return self._do_multipart_upload(bucket, key, size, part_size, storage_class, version_id)
380
+ return self._do_multipart_upload(bucket, key, size, part_size, storage_class, tags, version_id)
351
381
  else:
352
382
  # Force copy the object into standard in a single operation
353
383
  copy_source = {'Bucket': bucket, 'Key': key}
@@ -358,6 +388,8 @@ class GlacierUtils:
358
388
  if version_id:
359
389
  copy_source['VersionId'] = version_id
360
390
  copy_target['CopySourceVersionId'] = version_id
391
+ if tags:
392
+ copy_target['Tagging'] = tags
361
393
  response = self.s3.copy_object(CopySource=copy_source, **copy_target)
362
394
  PRINT(f'Response from boto3 copy:\n{response}')
363
395
  PRINT(f'Object {bucket}/{key} copied back to its original location in S3')
@@ -0,0 +1,257 @@
1
+ import os
2
+ import toml
3
+
4
+ from pkg_resources import resource_filename
5
+ from typing import Optional
6
+ from .env_utils import EnvUtils
7
+ from .misc_utils import classproperty
8
+
9
+
10
+ def project_filename(filename):
11
+ # TODO: In fact we should do this based on the working dir so that when this is imported to another repo,
12
+ # it gets the inserts out of that repo's tests, not our own.
13
+ return resource_filename(Project.PACKAGE_NAME, filename)
14
+
15
+
16
+ class ProjectRegistry:
17
+
18
+ SHOW_HERALD_WHEN_INITIALIZED = True
19
+
20
+ REGISTERED_PROJECTS = {}
21
+
22
+ # All of these might never be other than None so be careful when accessing them.
23
+ APPLICATION_PROJECT_HOME = None
24
+ PYPROJECT_TOML_FILE = None
25
+ PYPROJECT_TOML = None
26
+ POETRY_DATA = None
27
+ # This is expected to ultimately be set properly.
28
+ _PYPROJECT_NAME = None
29
+
30
+ @classproperty
31
+ def PYPROJECT_NAME(cls) -> str: # noQA - PyCharm thinks this should be 'self'
32
+ if cls._PYPROJECT_NAME is None:
33
+ cls.initialize_pyproject_name()
34
+ result: Optional[str] = cls._PYPROJECT_NAME
35
+ if result is None:
36
+ raise ValueError(f"ProjectRegistry.PROJECT_NAME not initialized properly.")
37
+ return result
38
+
39
+ @classmethod
40
+ def initialize_pyproject_name(cls, project_home=None, pyproject_toml_file=None):
41
+ if cls._PYPROJECT_NAME is None:
42
+ # This isn't the home of snovault, but the home of the snovault-based application.
43
+ # So in CGAP, for example, this would want to be the home of the CGAP application.
44
+ # If not set, it will be assumed that the current working directory is that.
45
+ if not project_home:
46
+ project_home = os.environ.get("APPLICATION_PROJECT_HOME", os.path.abspath(os.curdir))
47
+ cls.APPLICATION_PROJECT_HOME = project_home
48
+ if not pyproject_toml_file:
49
+ expected_pyproject_toml_file = os.path.join(project_home, "pyproject.toml")
50
+ pyproject_toml_file = (expected_pyproject_toml_file
51
+ if os.path.exists(expected_pyproject_toml_file)
52
+ else None)
53
+ cls.PYPROJECT_TOML_FILE = pyproject_toml_file
54
+ cls.PYPROJECT_TOML = pyproject_toml = (toml.load(cls.PYPROJECT_TOML_FILE)
55
+ if cls.PYPROJECT_TOML_FILE
56
+ else None)
57
+ cls.POETRY_DATA = (pyproject_toml['tool']['poetry']
58
+ if pyproject_toml
59
+ else None)
60
+
61
+ declared_pyproject_name = os.environ.get("APPLICATION_PYPROJECT_NAME")
62
+ inferred_pyproject_name = cls.POETRY_DATA['name'] if cls.POETRY_DATA else None
63
+ if (declared_pyproject_name and inferred_pyproject_name
64
+ and declared_pyproject_name != inferred_pyproject_name):
65
+ raise RuntimeError(f"APPLICATION_PYPROJECT_NAME={declared_pyproject_name!r},"
66
+ f" but {pyproject_toml_file} says it should be {inferred_pyproject_name!r}")
67
+
68
+ cls._PYPROJECT_NAME = declared_pyproject_name or inferred_pyproject_name
69
+
70
+ @classmethod
71
+ def register(cls, name):
72
+ """
73
+ Registers a class to be used based on the name in the top of pyproject.toml.
74
+ Note that this means that cgap-portal and fourfront will both register as 'encoded',
75
+ as in:
76
+
77
+ @Project.register('encoded')
78
+ class FourfrontProject(EncodedCoreProject):
79
+ PRETTY_NAME = "Fourfront"
80
+
81
+ Since fourfront and cgap-portal don't occupy the same space, no confusion should result.
82
+ """
83
+ def _wrap_class(the_class):
84
+ the_class_name = the_class.__name__
85
+ if not issubclass(the_class, Project):
86
+ raise ValueError(f"The class {the_class_name} must inherit from Project.")
87
+ lower_registry_name = name.lower()
88
+ for x in ['cgap-portal', 'fourfront', 'smaht']:
89
+ if x in lower_registry_name:
90
+ # It's an easy error to make, but the name of the project from which we're gaining foothold
91
+ # in pyproject.toml is 'encoded', not 'cgap-portal', etc., so the name 'encoded' will be
92
+ # needed for bootstrapping. So it should look like
93
+ # -kmp 15-May-2023
94
+ raise ValueError(f"Please use ProjectRegistry.register('encoded'),"
95
+ f" not ProjectRegistry.register({name!r})."
96
+ f" This registration is just for bootstrapping."
97
+ f" The class can still be {the_class_name}.")
98
+ cls.REGISTERED_PROJECTS[name] = the_class
99
+ return the_class
100
+ return _wrap_class
101
+
102
+ @classmethod
103
+ def _lookup(cls, name):
104
+ """
105
+ Returns the project object with the given name.
106
+
107
+ :param name: a string name that was used in a ProjectRegistry.register decorator
108
+
109
+ NOTE: There is no need for this function to be called outside of this class except for testing.
110
+ Really only one of these should be instantiated per running application, and that's
111
+ done automatically by this class.
112
+ """
113
+ project_class = cls.REGISTERED_PROJECTS.get(name)
114
+ return project_class
115
+
116
+ @classmethod
117
+ def _make_project(cls):
118
+ """
119
+ Creates and returns an instantiated project object for the current project.
120
+
121
+ The project to use can be specified by setting the environment variable APPLICATION_PROJECT_HOME
122
+ to a particular directory that contains the pyproject.toml file to use.
123
+ If no such variable is set, the current working directory is used.
124
+
125
+ NOTE: There is no need for this function to be called outside of this class except for testing.
126
+ Really only one of these should be instantiated per running application, and that's
127
+ done automatically by this class.
128
+ """
129
+ project_class = cls._lookup(cls.PYPROJECT_NAME)
130
+ assert issubclass(project_class, Project)
131
+ project: Project = project_class()
132
+ return project # instantiate and return
133
+
134
+ _app_project = None
135
+ _initialized = False
136
+
137
+ @classmethod
138
+ def initialize(cls):
139
+ if cls._initialized:
140
+ raise RuntimeError(f"{cls.__name__}.initialize() was called more than once.")
141
+ cls._app_project = cls._make_project()
142
+ cls._initalized = True
143
+ if cls.SHOW_HERALD_WHEN_INITIALIZED:
144
+ cls.show_herald()
145
+ app_project: Project = cls.app_project
146
+ return app_project # It's initialized now, so we use the proper interface
147
+
148
+ @classmethod
149
+ def show_herald(cls):
150
+ app_project = cls.app_project_maker()
151
+
152
+ print("=" * 80)
153
+ print(f"APPLICATION_PROJECT_HOME == {cls.APPLICATION_PROJECT_HOME!r}")
154
+ print(f"PYPROJECT_TOML_FILE == {cls.PYPROJECT_TOML_FILE!r}")
155
+ print(f"PYPROJECT_NAME == {cls.PYPROJECT_NAME!r}")
156
+ the_app_project = Project.app_project
157
+ the_app_project_class = the_app_project.__class__
158
+ the_app_project_class_name = the_app_project_class.__name__
159
+ assert (Project.app_project
160
+ == app_project()
161
+ == the_app_project_class.app_project
162
+ == the_app_project.app_project), (
163
+ "Project consistency check failed."
164
+ )
165
+ print(f"{the_app_project_class_name}.app_project == Project.app_project == app_project() == {app_project()!r}")
166
+ print(f"app_project().NAME == {app_project().NAME!r}")
167
+ print(f"app_project().PRETTY_NAME == {app_project().PRETTY_NAME!r}")
168
+ print(f"app_project().PACKAGE_NAME == {app_project().PACKAGE_NAME!r}")
169
+ print(f"app_project().APP_NAME == {app_project().APP_NAME!r}")
170
+ print(f"app_project().APP_PRETTY_NAME == {app_project().APP_PRETTY_NAME!r}")
171
+ print("=" * 80)
172
+
173
+ @classproperty
174
+ def app_project(cls): # noQA - PyCharm thinks we should use 'self'
175
+ """
176
+ Once the project is initialized, ProjectRegistry.app_project returns the application object
177
+ that should be used to dispatch project-dependent behavior.
178
+ """
179
+ if cls._app_project is None:
180
+ # You need to put a call to
181
+ raise RuntimeError(f"Attempt to access {cls.__name__}.project before .initialize() called.")
182
+ return cls._app_project
183
+
184
+ @classmethod
185
+ def app_project_maker(cls):
186
+
187
+ def app_project(initialize=False, initialization_options: Optional[dict] = None):
188
+ if initialize:
189
+ Project.initialize_app_project(**(initialization_options or {}))
190
+ return ProjectRegistry.app_project
191
+
192
+ return app_project
193
+
194
+
195
+ class Project:
196
+ """
197
+ A class that should be a superclass of all classes registered using ProjectRegistry.register
198
+
199
+ All such classes have these names:
200
+ .NAME - The name of the project in pyproject.toml
201
+ .PACKAGE_NAME - The pypi name of the project, useful for pkg_resources, for example.
202
+ .PRETTY_NAME - The pretty name of the package name
203
+ .APP_NAME - The ame of the project application (see dcicutils.common and the orchestrated app in EnvUtils)
204
+ .APP_PRETTY_NAME - The pretty name of the project application.
205
+
206
+ Some sample usess of pre-defined attributes of a Project that may help motivate the choice of attribute names:
207
+
208
+ registered |
209
+ name | NAME | PACKAGE_NAME | PRETTY NAME | APP_NAME | APP_PRETTY_NAME
210
+ -------------+--------------+----------------+--------------+-----------+----------------
211
+ snovault | dcicsnovault | snovault | Snovault | snovault | Snovault
212
+ encoded-core | encoded-core | encoded-core | Encoded Core | core | Core
213
+ encoded | cgap-portal | cgap-portal | CGAP Portal | cgap | CGAP
214
+ encoded | fourfront | fourfront | Fourfront | fourfront | Fourfront
215
+ encoded | smaht-portal | smaht-portal | SMaHT Portal | smaht | SMaHT
216
+
217
+ The registered name is the one used with the ProjectRegistry.register() decorator.
218
+ """
219
+
220
+ NAME = 'project'
221
+
222
+ @classmethod
223
+ def _prettify(cls, name):
224
+ return name.title().replace("Cgap", "CGAP").replace("Smaht", "SMaHT").replace("-", " ")
225
+
226
+ @classproperty
227
+ def PACKAGE_NAME(cls): # noQA - PyCharm wants the variable name to be self
228
+ return cls.NAME.replace('dcic', '')
229
+
230
+ @classproperty
231
+ def PRETTY_NAME(cls): # noQA - PyCharm wants the variable name to be self
232
+ return cls._prettify(cls.PACKAGE_NAME)
233
+
234
+ @classproperty
235
+ def APP_NAME(cls): # noQA - PyCharm wants the variable name to be self
236
+ return cls.PACKAGE_NAME.replace('-portal', '').replace('encoded-', '')
237
+
238
+ @classproperty
239
+ def APP_PRETTY_NAME(cls): # noQA - PyCharm wants the variable name to be self
240
+ return cls._prettify(cls.APP_NAME)
241
+
242
+ @classproperty
243
+ def app_project(cls): # noQA - PyCharm wants the variable name to be self
244
+ """
245
+ Project.app_project returns the actual instantiated project for app-specific behavior,
246
+ which might be of this class or one of its subclasses.
247
+
248
+ This access will fail if the project has not been initialized.
249
+ """
250
+ return ProjectRegistry.app_project
251
+
252
+ @classmethod
253
+ def initialize_app_project(cls, initialize_env_utils=True):
254
+ if initialize_env_utils:
255
+ EnvUtils.init()
256
+ project: Project = ProjectRegistry.initialize()
257
+ return project
@@ -462,6 +462,8 @@ class s3Utils(s3Base): # NOQA - This class name violates style rules, but a lot
462
462
  content_type = mimetypes.guess_type(upload_key)[0]
463
463
  if content_type is None:
464
464
  content_type = 'binary/octet-stream'
465
+ if isinstance(obj, dict):
466
+ obj = json.dumps(obj)
465
467
  if acl:
466
468
  # we use this to set some of the object as public
467
469
  return self.s3.put_object(Bucket=self.outfile_bucket,
@@ -480,6 +482,8 @@ class s3Utils(s3Base): # NOQA - This class name violates style rules, but a lot
480
482
  bucket = self.sys_bucket
481
483
  if not secret:
482
484
  secret = os.environ["S3_ENCRYPT_KEY"]
485
+ if isinstance(data, dict):
486
+ data = json.dumps(data)
483
487
  return self.s3.put_object(Bucket=bucket,
484
488
  Key=keyname,
485
489
  Body=data,
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dcicutils"
3
- version = "7.4.1"
3
+ version = "7.4.2.1b0" # to become 7.5.0
4
4
  description = "Utility package for interacting with the 4DN Data Portal and other 4DN resources"
5
5
  authors = ["4DN-DCIC Team <support@4dnucleome.org>"]
6
6
  license = "MIT"
@@ -38,7 +38,7 @@ entry_points = \
38
38
 
39
39
  setup_kwargs = {
40
40
  'name': 'dcicutils',
41
- 'version': '7.4.1',
41
+ 'version': '7.4.2.1b0',
42
42
  'description': 'Utility package for interacting with the 4DN Data Portal and other 4DN resources',
43
43
  'long_description': '=====\nutils\n=====\n\nCheck out our full documentation `here <https://dcic-utils.readthedocs.io/en/latest/>`_\n\nThis repository contains various utility modules shared amongst several projects in the 4DN-DCIC. It is meant to be used internally by the DCIC team and externally as a Python API to `Fourfront <https://data.4dnucleome.org>`_\\ , the 4DN data portal.\n\npip installable as the ``dcicutils`` package with: ``pip install dcicutils``\n\nSee `this document <https://dcic-utils.readthedocs.io/en/latest/getting_started.html>`_ for tips on getting started. `Go here <https://dcic-utils.readthedocs.io/en/latest/examples.html>`_ for examples of some of the most useful functions.\n\n\n.. image:: https://travis-ci.org/4dn-dcic/utils.svg?branch=master\n :target: https://travis-ci.org/4dn-dcic/utils\n :alt: Build Status\n\n\n.. image:: https://coveralls.io/repos/github/4dn-dcic/utils/badge.svg?branch=master\n :target: https://coveralls.io/github/4dn-dcic/utils?branch=master\n :alt: Coverage\n\n.. image:: https://readthedocs.org/projects/dcic-utils/badge/?version=latest\n :target: https://dcic-utils.readthedocs.io/en/latest/?badge=latest\n :alt: Documentation Status\n',
44
44
  'author': '4DN-DCIC Team',
File without changes
File without changes