deriva 1.7.0__tar.gz → 1.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. deriva-1.7.1/CHANGELOG.md +18 -0
  2. {deriva-1.7.0/deriva.egg-info → deriva-1.7.1}/PKG-INFO +2 -2
  3. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/__init__.py +1 -1
  4. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/catalog_cli.py +83 -39
  5. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/datapath.py +314 -3
  6. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/__init__.py +4 -2
  7. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/__init__.py +4 -0
  8. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/deriva_download.py +33 -13
  9. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/deriva_download_cli.py +3 -2
  10. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/query/base_query_processor.py +9 -4
  11. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/upload/__init__.py +4 -0
  12. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/upload/deriva_upload.py +4 -0
  13. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/upload/deriva_upload_cli.py +2 -2
  14. {deriva-1.7.0 → deriva-1.7.1/deriva.egg-info}/PKG-INFO +2 -2
  15. {deriva-1.7.0 → deriva-1.7.1}/deriva.egg-info/requires.txt +1 -1
  16. {deriva-1.7.0 → deriva-1.7.1}/setup.py +1 -1
  17. {deriva-1.7.0 → deriva-1.7.1}/tests/deriva/core/test_datapath.py +24 -2
  18. deriva-1.7.0/CHANGELOG.md +0 -11
  19. {deriva-1.7.0 → deriva-1.7.1}/.gitignore +0 -0
  20. {deriva-1.7.0 → deriva-1.7.1}/LICENSE +0 -0
  21. {deriva-1.7.0 → deriva-1.7.1}/README.md +0 -0
  22. {deriva-1.7.0 → deriva-1.7.1}/deriva/__init__.py +0 -0
  23. {deriva-1.7.0 → deriva-1.7.1}/deriva/config/__init__.py +0 -0
  24. {deriva-1.7.0 → deriva-1.7.1}/deriva/config/acl_config.py +0 -0
  25. {deriva-1.7.0 → deriva-1.7.1}/deriva/config/annotation_config.py +0 -0
  26. {deriva-1.7.0 → deriva-1.7.1}/deriva/config/annotation_validate.py +0 -0
  27. {deriva-1.7.0 → deriva-1.7.1}/deriva/config/base_config.py +0 -0
  28. {deriva-1.7.0 → deriva-1.7.1}/deriva/config/dump_catalog_annotations.py +0 -0
  29. {deriva-1.7.0 → deriva-1.7.1}/deriva/config/examples/group_owner_policy.json +0 -0
  30. {deriva-1.7.0 → deriva-1.7.1}/deriva/config/examples/self_serve_policy.json +0 -0
  31. {deriva-1.7.0 → deriva-1.7.1}/deriva/config/rollback_annotation.py +0 -0
  32. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/annotation.py +0 -0
  33. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/base_cli.py +0 -0
  34. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/deriva_binding.py +0 -0
  35. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/deriva_server.py +0 -0
  36. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/ermrest_catalog.py +0 -0
  37. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/ermrest_model.py +0 -0
  38. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/hatrac_cli.py +0 -0
  39. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/hatrac_store.py +0 -0
  40. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/polling_ermrest_catalog.py +0 -0
  41. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/app_links.schema.json +0 -0
  42. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/asset.schema.json +0 -0
  43. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/bulk_upload.schema.json +0 -0
  44. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/chaise_config.schema.json +0 -0
  45. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/citation.schema.json +0 -0
  46. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/column_display.schema.json +0 -0
  47. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/display.schema.json +0 -0
  48. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/export.schema.json +0 -0
  49. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/export_2019.schema.json +0 -0
  50. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/foreign_key.schema.json +0 -0
  51. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/generated.schema.json +0 -0
  52. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/immutable.schema.json +0 -0
  53. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/indexing_preferences.schema.json +0 -0
  54. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/key_display.schema.json +0 -0
  55. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/non_deletable.schema.json +0 -0
  56. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/required.schema.json +0 -0
  57. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/source_definitions.schema.json +0 -0
  58. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/table_alternatives.schema.json +0 -0
  59. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/table_display.schema.json +0 -0
  60. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/visible_columns.schema.json +0 -0
  61. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/schemas/visible_foreign_keys.schema.json +0 -0
  62. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/tests/__init__.py +0 -0
  63. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/utils/__init__.py +0 -0
  64. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/utils/core_utils.py +0 -0
  65. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/utils/globus_auth_utils.py +0 -0
  66. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/utils/hash_utils.py +0 -0
  67. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/utils/mime_utils.py +0 -0
  68. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/utils/version_utils.py +0 -0
  69. {deriva-1.7.0 → deriva-1.7.1}/deriva/core/utils/webauthn_utils.py +0 -0
  70. {deriva-1.7.0 → deriva-1.7.1}/deriva/seo/README.md +0 -0
  71. {deriva-1.7.0 → deriva-1.7.1}/deriva/seo/__init__.py +0 -0
  72. {deriva-1.7.0 → deriva-1.7.1}/deriva/seo/sitemap_builder.py +0 -0
  73. {deriva-1.7.0 → deriva-1.7.1}/deriva/seo/sitemap_cli.py +0 -0
  74. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/backup/__init__.py +0 -0
  75. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/backup/__main__.py +0 -0
  76. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/backup/deriva_backup.py +0 -0
  77. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/backup/deriva_backup_cli.py +0 -0
  78. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/__main__.py +0 -0
  79. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/__init__.py +0 -0
  80. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/base_processor.py +0 -0
  81. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/postprocess/__init__.py +0 -0
  82. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/postprocess/identifier_post_processor.py +0 -0
  83. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/postprocess/transfer_post_processor.py +0 -0
  84. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/postprocess/url_post_processor.py +0 -0
  85. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/query/__init__.py +0 -0
  86. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/query/bag_fetch_query_processor.py +0 -0
  87. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/query/file_download_query_processor.py +0 -0
  88. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/transform/__init__.py +0 -0
  89. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/transform/base_transform_processor.py +0 -0
  90. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/transform/column_transform_processor.py +0 -0
  91. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/transform/fasta_transform_processor.py +0 -0
  92. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/transform/format_transform_processor.py +0 -0
  93. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/transform/geo_transform_processor.py +0 -0
  94. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/processors/transform/string_transform_processor.py +0 -0
  95. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/__init__.py +0 -0
  96. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test1.json +0 -0
  97. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test10.json +0 -0
  98. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test11.json +0 -0
  99. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test12.json +0 -0
  100. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test13.json +0 -0
  101. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test14.json +0 -0
  102. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test15.json +0 -0
  103. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test16.json +0 -0
  104. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test19.json +0 -0
  105. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test2.json +0 -0
  106. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test20.json +0 -0
  107. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test3.json +0 -0
  108. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test4.json +0 -0
  109. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test5.json +0 -0
  110. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test6.json +0 -0
  111. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test7.json +0 -0
  112. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test8.json +0 -0
  113. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/download/tests/test9.json +0 -0
  114. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/restore/__init__.py +0 -0
  115. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/restore/__main__.py +0 -0
  116. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/restore/deriva_restore.py +0 -0
  117. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/restore/deriva_restore_cli.py +0 -0
  118. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/upload/__main__.py +0 -0
  119. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/upload/processors/__init__.py +0 -0
  120. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/upload/processors/archive_processor.py +0 -0
  121. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/upload/processors/base_processor.py +0 -0
  122. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/upload/processors/logging_processor.py +0 -0
  123. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/upload/processors/metadata_update_processor.py +0 -0
  124. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/upload/processors/rename_processor.py +0 -0
  125. {deriva-1.7.0 → deriva-1.7.1}/deriva/transfer/upload/tests/__init__.py +0 -0
  126. {deriva-1.7.0 → deriva-1.7.1}/deriva/utils/__init__.py +0 -0
  127. {deriva-1.7.0 → deriva-1.7.1}/deriva.egg-info/SOURCES.txt +0 -0
  128. {deriva-1.7.0 → deriva-1.7.1}/deriva.egg-info/dependency_links.txt +0 -0
  129. {deriva-1.7.0 → deriva-1.7.1}/deriva.egg-info/entry_points.txt +0 -0
  130. {deriva-1.7.0 → deriva-1.7.1}/deriva.egg-info/top_level.txt +0 -0
  131. {deriva-1.7.0 → deriva-1.7.1}/docs/BUILD.md +0 -0
  132. {deriva-1.7.0 → deriva-1.7.1}/docs/Makefile +0 -0
  133. {deriva-1.7.0 → deriva-1.7.1}/docs/README.md +0 -0
  134. {deriva-1.7.0 → deriva-1.7.1}/docs/_static/README.txt +0 -0
  135. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.config.rst +0 -0
  136. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.core.rst +0 -0
  137. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.core.utils.rst +0 -0
  138. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.rst +0 -0
  139. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.seo.rst +0 -0
  140. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.transfer.backup.rst +0 -0
  141. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.transfer.download.processors.postprocess.rst +0 -0
  142. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.transfer.download.processors.query.rst +0 -0
  143. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.transfer.download.processors.rst +0 -0
  144. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.transfer.download.processors.transform.rst +0 -0
  145. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.transfer.download.rst +0 -0
  146. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.transfer.restore.rst +0 -0
  147. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.transfer.rst +0 -0
  148. {deriva-1.7.0 → deriva-1.7.1}/docs/api/deriva.transfer.upload.rst +0 -0
  149. {deriva-1.7.0 → deriva-1.7.1}/docs/cli/commands.md +0 -0
  150. {deriva-1.7.0 → deriva-1.7.1}/docs/cli/deriva-acl-config.md +0 -0
  151. {deriva-1.7.0 → deriva-1.7.1}/docs/cli/deriva-annotation-config.md +0 -0
  152. {deriva-1.7.0 → deriva-1.7.1}/docs/cli/deriva-annotation-validate.md +0 -0
  153. {deriva-1.7.0 → deriva-1.7.1}/docs/cli/deriva-backup-cli.md +0 -0
  154. {deriva-1.7.0 → deriva-1.7.1}/docs/cli/deriva-download-cli.md +0 -0
  155. {deriva-1.7.0 → deriva-1.7.1}/docs/cli/deriva-hatrac-cli.md +0 -0
  156. {deriva-1.7.0 → deriva-1.7.1}/docs/cli/deriva-restore-cli.md +0 -0
  157. {deriva-1.7.0 → deriva-1.7.1}/docs/cli/deriva-sitemap-cli.md +0 -0
  158. {deriva-1.7.0 → deriva-1.7.1}/docs/conf.py +0 -0
  159. {deriva-1.7.0 → deriva-1.7.1}/docs/derivapy-catalog-snapshot.ipynb +0 -0
  160. {deriva-1.7.0 → deriva-1.7.1}/docs/derivapy-catalog.ipynb +0 -0
  161. {deriva-1.7.0 → deriva-1.7.1}/docs/derivapy-datapath-example-1.ipynb +0 -0
  162. {deriva-1.7.0 → deriva-1.7.1}/docs/derivapy-datapath-example-2.ipynb +0 -0
  163. {deriva-1.7.0 → deriva-1.7.1}/docs/derivapy-datapath-example-3.ipynb +0 -0
  164. {deriva-1.7.0 → deriva-1.7.1}/docs/derivapy-datapath-example-4.ipynb +0 -0
  165. {deriva-1.7.0 → deriva-1.7.1}/docs/derivapy-datapath-update.ipynb +0 -0
  166. {deriva-1.7.0 → deriva-1.7.1}/docs/get-started.ipynb +0 -0
  167. {deriva-1.7.0 → deriva-1.7.1}/docs/index.rst +0 -0
  168. {deriva-1.7.0 → deriva-1.7.1}/docs/install.md +0 -0
  169. {deriva-1.7.0 → deriva-1.7.1}/docs/make.bat +0 -0
  170. {deriva-1.7.0 → deriva-1.7.1}/docs/project-tutorial.md +0 -0
  171. {deriva-1.7.0 → deriva-1.7.1}/docs/using-r.md +0 -0
  172. {deriva-1.7.0 → deriva-1.7.1}/requirements_dev.txt +0 -0
  173. {deriva-1.7.0 → deriva-1.7.1}/setup.cfg +0 -0
  174. {deriva-1.7.0 → deriva-1.7.1}/tests/__init__.py +0 -0
  175. {deriva-1.7.0 → deriva-1.7.1}/tests/deriva/__init__.py +0 -0
  176. {deriva-1.7.0 → deriva-1.7.1}/tests/deriva/core/__init__.py +0 -0
@@ -0,0 +1,18 @@
1
+ # CHANGE LOG
2
+
3
+ ## 1.6.5
4
+
5
+ DataPath feature enhancements and changes:
6
+ * new `denormalize()` method that uses a `visible-columns` "context" or a heuristic function to denormalize a path expression.
7
+ * new `limit()` method as a chaining operation so that clients don't need to explicity `fetch()` results with limits.
8
+ * subtle change to `link()` method for how it determines whether an alias name has been "bound" to the path. Now, it more explicitly checks if an alias object is found in the path's bound table instances. This change also relaxes the alias naming constraint to allow for automatic conflict resolution by appending an incremental numeric disambiguator if the given alias name is already in use. This could result in some `link()` methods now succeeding where they would have failed in the past.
9
+
10
+ ## 1.0.0
11
+ Feature release.
12
+ * Refactored catalog configuration API.
13
+ * Support for aggregate functions and binning in DataPath API.
14
+ * Various bug fixes.
15
+ * Complete change list [here](https://github.com/informatics-isi-edu/deriva-py/compare/v0.9.0...v1.0.0).
16
+
17
+ Change lists for previous releases can be found on the GitHub releases
18
+ page [here](https://github.com/informatics-isi-edu/deriva-py/releases).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: deriva
3
- Version: 1.7.0
3
+ Version: 1.7.1
4
4
  Summary: Python APIs and CLIs (Command-Line Interfaces) for the DERIVA platform.
5
5
  Home-page: https://github.com/informatics-isi-edu/deriva-py
6
6
  Author: USC Information Sciences Institute, Informatics Systems Research Division
@@ -29,7 +29,7 @@ Requires-Dist: certifi
29
29
  Requires-Dist: pika
30
30
  Requires-Dist: urllib3<3,>=1.26
31
31
  Requires-Dist: portalocker>=1.2.1
32
- Requires-Dist: bdbag>=1.7.2
32
+ Requires-Dist: bdbag>=1.7.3
33
33
  Requires-Dist: globus_sdk<4,>=3
34
34
  Requires-Dist: fair-research-login>=0.3.1
35
35
  Requires-Dist: fair-identifiers-client>=0.5.1
@@ -1,4 +1,4 @@
1
- __version__ = "1.7.0"
1
+ __version__ = "1.7.1"
2
2
 
3
3
  from deriva.core.utils.core_utils import *
4
4
  from deriva.core.base_cli import BaseCLI, KeyValuePairArgs
@@ -55,21 +55,21 @@ class DerivaCatalogCLI (BaseCLI):
55
55
 
56
56
  # parent arg parser
57
57
  self.remove_options(['--config-file', '--credential-file'])
58
- self.parser.add_argument("--protocol", choices=["http", "https"], default='https',
58
+ self.parser.add_argument("-p", "--protocol", choices=["http", "https"], default='https',
59
59
  help="transport protocol: 'http' or 'https'")
60
60
  subparsers = self.parser.add_subparsers(title='sub-commands', dest='subcmd')
61
61
 
62
62
  # exists parser
63
63
  exists_parser = subparsers.add_parser('exists', help="Check if catalog exists.")
64
- exists_parser.add_argument("id", metavar="<id>", type=str, help="catalog id")
64
+ exists_parser.add_argument("id", metavar="<id>", type=str, help="Catalog ID")
65
65
  exists_parser.set_defaults(func=self.catalog_exists)
66
66
 
67
67
  # create parser
68
68
  create_parser = subparsers.add_parser('create', help="Create a new catalog.")
69
- create_parser.add_argument("--id", metavar="<id>", type=str, help="catalog id")
70
- create_parser.add_argument("--owner", metavar="<owner> <owner> ...",
69
+ create_parser.add_argument("--id", metavar="<id>", type=str, help="Catalog ID")
70
+ create_parser.add_argument("-o", "--owner", metavar="<owner> <owner> ...",
71
71
  nargs="+", help="List of quoted user or group identifier strings.")
72
- create_parser.add_argument("--auto-configure", action="store_true",
72
+ create_parser.add_argument("-a", "--auto-configure", action="store_true",
73
73
  help="Configure the new catalog with a set of baseline defaults")
74
74
  create_parser.add_argument("--configure-args", metavar="[key=value key=value ...]",
75
75
  nargs='+', action=KeyValuePairArgs, default={},
@@ -80,11 +80,12 @@ class DerivaCatalogCLI (BaseCLI):
80
80
 
81
81
  # get parser
82
82
  get_parser = subparsers.add_parser('get', help="Send a HTTP GET request to the catalog.")
83
- get_parser.add_argument("id", metavar="<id>", type=str, help="catalog id")
83
+ get_parser.add_argument("id", metavar="<id>", type=str, help="Catalog ID")
84
84
  get_parser.add_argument("path", metavar="<request-path>", help="The ERMRest API path.")
85
- get_parser.add_argument("--output-file", metavar="<output file path>", help="Path to output file.")
86
- get_parser.add_argument("--output-format", choices=["json", "json-stream", "csv"], default="json")
87
- get_parser.add_argument("--auto-delete", action="store_true",
85
+ get_parser.add_argument("-o", "--output-file", metavar="<output file path>", help="Path to output file.")
86
+ get_parser.add_argument("-f", "--output-format", choices=["json", "json-stream", "csv"], default="json",
87
+ help="The output file format. Defaults to 'json'")
88
+ get_parser.add_argument("-a", "--auto-delete", action="store_true",
88
89
  help="Automatically delete output file if no results are returned.")
89
90
  get_parser.add_argument("--headers", metavar="[key=value key=value ...]",
90
91
  nargs='+', action=KeyValuePairArgs, default={},
@@ -95,10 +96,12 @@ class DerivaCatalogCLI (BaseCLI):
95
96
 
96
97
  # put parser
97
98
  put_parser = subparsers.add_parser('put', help="Send a HTTP PUT request to the catalog.")
98
- put_parser.add_argument("id", metavar="<id>", type=str, help="catalog id")
99
+ put_parser.add_argument("id", metavar="<id>", type=str, help="Catalog ID")
99
100
  put_parser.add_argument("path", metavar="<request-path>", help="The ERMRest API path.")
100
- put_parser.add_argument("--input-file", metavar="<output file path>", help="Path to input file.")
101
- put_parser.add_argument("--input-format", choices=["json", "json-stream", "csv"], default="json")
101
+ put_parser.add_argument("input-file", metavar="<input file path>",
102
+ help="Path to an input file containing the request message body.")
103
+ put_parser.add_argument("-f", "--input-format", choices=["json", "json-stream", "csv"], default="json",
104
+ help="The input file format. Defaults to 'json'")
102
105
  put_parser.add_argument("--headers", metavar="[key=value key=value ...]",
103
106
  nargs='+', action=KeyValuePairArgs, default={},
104
107
  help="Variable length of whitespace-delimited key=value pair arguments used for "
@@ -108,10 +111,12 @@ class DerivaCatalogCLI (BaseCLI):
108
111
 
109
112
  # post parser
110
113
  post_parser = subparsers.add_parser('post', help="Send a HTTP POST request to the catalog.")
111
- post_parser.add_argument("id", metavar="<id>", type=str, help="catalog id")
114
+ post_parser.add_argument("id", metavar="<id>", type=str, help="Catalog ID")
112
115
  post_parser.add_argument("path", metavar="<request-path>", help="The ERMRest API path.")
113
- post_parser.add_argument("--input-file", metavar="<input file path>", help="Path to input file.")
114
- post_parser.add_argument("--input-format", choices=["json", "json-stream", "csv"], default="json")
116
+ post_parser.add_argument("input-file", metavar="<input file path>",
117
+ help="Path to an input file containing the request message body.")
118
+ post_parser.add_argument("-f", "--input-format", choices=["json", "json-stream", "csv"], default="json",
119
+ help="The input file format. Defaults to 'json'")
115
120
  post_parser.add_argument("--headers", metavar="[key=value key=value ...]",
116
121
  nargs='+', action=KeyValuePairArgs, default={},
117
122
  help="Variable length of whitespace-delimited key=value pair arguments used for "
@@ -122,7 +127,7 @@ class DerivaCatalogCLI (BaseCLI):
122
127
  # delete parser
123
128
  del_parser = subparsers.add_parser('delete', help="Send a HTTP DELETE request to the catalog. "
124
129
  "Use the 'drop' command to delete the entire catalog.")
125
- del_parser.add_argument("id", metavar="<id>", type=str, help="catalog id")
130
+ del_parser.add_argument("id", metavar="<id>", type=str, help="Catalog ID")
126
131
  del_parser.add_argument("path", metavar="<request-path>", help="The ERMRest API path.")
127
132
  del_parser.add_argument("--headers", metavar="[key=value key=value ...]",
128
133
  nargs='+', action=KeyValuePairArgs, default={},
@@ -133,12 +138,12 @@ class DerivaCatalogCLI (BaseCLI):
133
138
 
134
139
  # drop parser
135
140
  drop_parser = subparsers.add_parser('drop', help="Delete a catalog.")
136
- drop_parser.add_argument("id", metavar="<id>", type=str, help="catalog id")
141
+ drop_parser.add_argument("id", metavar="<id>", type=str, help="Catalog ID")
137
142
  drop_parser.set_defaults(func=self.catalog_drop)
138
143
 
139
144
  # clone parser
140
145
  clone_parser = subparsers.add_parser('clone', help="Clone a source catalog to a new destination catalog.")
141
- clone_parser.add_argument("id", metavar="<id>", type=str, help="catalog id")
146
+ clone_parser.add_argument("id", metavar="<id>", type=str, help="Catalog ID")
142
147
  clone_parser.add_argument("--no-copy-data", action="store_false",
143
148
  help="Do not copy table contents.")
144
149
  clone_parser.add_argument("--no-copy-annotations", action="store_false",
@@ -154,8 +159,8 @@ class DerivaCatalogCLI (BaseCLI):
154
159
  # create_alias parser
155
160
  create_alias_parser = subparsers.add_parser('create-alias', help="Create a new catalog alias")
156
161
  create_alias_parser.add_argument("--id", metavar="<id>", type=str, help="The alias id.")
157
- create_alias_parser.add_argument("--alias-target", metavar="<alias>", help="The target catalog id.")
158
- create_alias_parser.add_argument("--owner", metavar="<owner> <owner> ...",
162
+ create_alias_parser.add_argument("-t", "--alias-target", metavar="<alias>", help="The target catalog id.")
163
+ create_alias_parser.add_argument("-o", "--owner", metavar="<owner> <owner> ...",
159
164
  nargs="+", help="List of quoted user or group identifier strings.")
160
165
  create_alias_parser.set_defaults(func=self.catalog_alias_create)
161
166
 
@@ -167,10 +172,10 @@ class DerivaCatalogCLI (BaseCLI):
167
172
  # update_alias parser
168
173
  update_alias_parser = subparsers.add_parser('update-alias', help="Update an existing catalog alias")
169
174
  update_alias_parser.add_argument("--id", metavar="<id>", type=str, help="The alias id.")
170
- update_alias_parser.add_argument("--alias-target", metavar="<alias>", nargs='?', default=nochange, const=None,
175
+ update_alias_parser.add_argument("-t", "--alias-target", metavar="<alias>", nargs='?', default=nochange, const=None,
171
176
  help="The target catalog id. If specified without a catalog id as an argument "
172
177
  "value, the existing alias target will be cleared ")
173
- update_alias_parser.add_argument("--owner", metavar="<owner> <owner> ...", nargs='+', default=nochange,
178
+ update_alias_parser.add_argument("-o", "--owner", metavar="<owner> <owner> ...", nargs='+', default=nochange,
174
179
  help="List of quoted user or group identifier strings.")
175
180
  update_alias_parser.set_defaults(func=self.catalog_alias_update)
176
181
 
@@ -194,9 +199,29 @@ class DerivaCatalogCLI (BaseCLI):
194
199
  self.id = args.id
195
200
  self.server = DerivaServer(self.protocol,
196
201
  args.host,
197
- credentials=DerivaCatalogCLI._get_credential(self.host,
198
- token=args.token,
199
- oauth2_token=args.oauth2_token))
202
+ credentials=DerivaCatalogCLI._get_credential(
203
+ self.host,
204
+ token=args.token,
205
+ oauth2_token=args.oauth2_token))
206
+
207
+ @staticmethod
208
+ def _decorate_headers(headers, file_format, method="get"):
209
+
210
+ header_format_map = {
211
+ "json": "application/json",
212
+ "json-stream": "application/x-json-stream",
213
+ "csv": "text/csv"
214
+ }
215
+
216
+ format_type = header_format_map.get(file_format)
217
+ if format_type is None:
218
+ raise UsageException("Unsupported format: %s" % file_format)
219
+ if str(method).lower() in ["get", "head"]:
220
+ headers["accept"] = format_type
221
+ elif str(method).lower() in ["post", "put"]:
222
+ headers["content-type"] = format_type
223
+ else:
224
+ raise UsageException("Unsupported method: %s" % method)
200
225
 
201
226
  def catalog_exists(self, args):
202
227
  """Implements the catalog_exists sub-command.
@@ -250,16 +275,7 @@ class DerivaCatalogCLI (BaseCLI):
250
275
  """
251
276
  headers = DEFAULT_HEADERS.copy()
252
277
  headers.update(args.headers)
253
-
254
- if args.output_format == "json":
255
- headers["accept"] = "application/json"
256
- elif args.output_format == "json-stream":
257
- headers["accept"] = "application/x-json-stream"
258
- elif args.output_format == "csv":
259
- headers["accept"] = "text/csv"
260
- else:
261
- raise UsageException("Unsupported output format: %s" % args.output_format)
262
-
278
+ self._decorate_headers(headers, args.output_format)
263
279
  catalog = self.server.connect_ermrest(args.id)
264
280
  try:
265
281
  if args.output_file:
@@ -278,13 +294,41 @@ class DerivaCatalogCLI (BaseCLI):
278
294
  os.remove(args.output_file)
279
295
  raise
280
296
 
281
- # TODO: implement PUT at some point
282
297
  def catalog_put(self, args):
283
- raise NotImplementedError
298
+ """Implements the catalog_put sub-command.
299
+ """
300
+ headers = DEFAULT_HEADERS.copy()
301
+ headers.update(args.headers)
302
+ self._decorate_headers(headers, args.input_format, "put")
303
+ try:
304
+ catalog = self.server.connect_ermrest(args.id)
305
+ with open(args.input_file, "rb") as input_file:
306
+ resp = catalog.put(args.path, data=input_file, headers=headers)
307
+ if not args.quiet:
308
+ pp(resp.json())
309
+ except HTTPError as e:
310
+ if e.response.status_code == requests.codes.not_found:
311
+ raise ResourceException('Catalog not found', e)
312
+ else:
313
+ raise e
284
314
 
285
- # TODO: implement POST at some point
286
315
  def catalog_post(self, args):
287
- raise NotImplementedError
316
+ """Implements the catalog_post sub-command.
317
+ """
318
+ headers = DEFAULT_HEADERS.copy()
319
+ headers.update(args.headers)
320
+ self._decorate_headers(headers, args.input_format, "post")
321
+ try:
322
+ catalog = self.server.connect_ermrest(args.id)
323
+ with open(args.input_file, "rb") as input_file:
324
+ resp = catalog.post(args.path, data=input_file, headers=headers)
325
+ if not args.quiet:
326
+ pp(resp.json())
327
+ except HTTPError as e:
328
+ if e.response.status_code == requests.codes.not_found:
329
+ raise ResourceException('Catalog not found', e)
330
+ else:
331
+ raise e
288
332
 
289
333
  def catalog_delete(self, args):
290
334
  """Implements the catalog_delete sub-command.
@@ -395,7 +395,8 @@ class DataPath (object):
395
395
  By default links use inner join semantics on the foreign key / key equality comparison. The `join_type`
396
396
  parameter can be used to specify `left`, `right`, or `full` outer join semantics.
397
397
 
398
- :param right: the right hand table of the link expression
398
+ :param right: the right hand table of the link expression; if the table or alias name is in use, an incremental
399
+ number will be used to disambiguate tables instances of the same original name.
399
400
  :param on: an equality comparison between key and foreign key columns, a conjunction of such comparisons, or a foreign key object
400
401
  :param join_type: the join type of this link which may be 'left', 'right', 'full' outer joins or '' for inner
401
402
  join link by default.
@@ -413,7 +414,7 @@ class DataPath (object):
413
414
  raise ValueError("'on' must be specified for outer joins")
414
415
  if right._schema._catalog != self._root._schema._catalog:
415
416
  raise ValueError("'right' is from a different catalog. Cannot link across catalogs.")
416
- if isinstance(right, _TableAlias) and right._name in self._table_instances:
417
+ if isinstance(right, _TableAlias) and right._parent == self:
417
418
  raise ValueError("'right' is a table alias that has already been used.")
418
419
  else:
419
420
  # Generate an unused alias name for the table
@@ -606,6 +607,18 @@ class DataPath (object):
606
607
 
607
608
  return self
608
609
 
610
+ def denormalize(self, context_name=None, heuristic=None, groupkey_name='RID'):
611
+ """Denormalizes a path based on a visible-columns annotation 'context' or a heuristic approach.
612
+
613
+ This method does not mutate this object. It returns a result set representing the denormalization of the path.
614
+
615
+ :param context_name: name of the visible-columns context or if none given, will attempt apply heuristics
616
+ :param heuristic: heuristic to apply if no context name specified
617
+ :param groupkey_name: column name for the group by key of the generated query expression (default: 'RID')
618
+ :return: a results set.
619
+ """
620
+ return _datapath_denormalize(self, context_name=context_name, heuristic=heuristic, groupkey_name=groupkey_name)
621
+
609
622
 
610
623
  class _ResultSet (object):
611
624
  """A set of results for various queries or data manipulations.
@@ -623,6 +636,7 @@ class _ResultSet (object):
623
636
  self._fetcher_fn = fetcher_fn
624
637
  self._results_doc = None
625
638
  self._sort_keys = None
639
+ self._limit = None
626
640
  self.uri = uri
627
641
 
628
642
  @property
@@ -656,6 +670,19 @@ class _ResultSet (object):
656
670
  self._results_doc = None
657
671
  return self
658
672
 
673
+ def limit(self, n):
674
+ """Set a limit on the number of results to be returned.
675
+
676
+ :param n: integer or None.
677
+ :return: self
678
+ """
679
+ try:
680
+ self._limit = None if n is None else int(n)
681
+ self._results_doc = None
682
+ return self
683
+ except ValueError:
684
+ raise ValueError('limit argument "n" must be an integer or None')
685
+
659
686
  def fetch(self, limit=None, headers=DEFAULT_HEADERS):
660
687
  """Fetches the results from the catalog.
661
688
 
@@ -663,7 +690,7 @@ class _ResultSet (object):
663
690
  :param headers: headers to send in request to server
664
691
  :return: self
665
692
  """
666
- limit = int(limit) if limit else None
693
+ limit = int(limit) if limit else self._limit
667
694
  self._results_doc = self._fetcher_fn(limit, self._sort_keys, headers)
668
695
  logger.debug("Fetched %d entities" % len(self._results_doc))
669
696
  return self
@@ -797,6 +824,18 @@ class _TableWrapper (object):
797
824
  """
798
825
  return _AttributeGroup(self, self._query, keys)
799
826
 
827
+ def denormalize(self, context_name=None, heuristic=None, groupkey_name='RID'):
828
+ """Denormalizes a path based on a visible-columns annotation 'context' or a heuristic approach.
829
+
830
+ This method does not mutate this object. It returns a result set representing the denormalization of the path.
831
+
832
+ :param context_name: name of the visible-columns context or if none given, will attempt apply heuristics
833
+ :param heuristic: heuristic to apply if no context name specified
834
+ :param groupkey_name: column name for the group by key of the generated query expression (default: 'RID')
835
+ :return: a results set.
836
+ """
837
+ return self.path.denormalize(context_name=context_name, heuristic=heuristic, groupkey_name=groupkey_name)
838
+
800
839
  def insert(self, entities, defaults=set(), nondefaults=set(), add_system_defaults=True, on_conflict_skip=False):
801
840
  """Inserts entities into the table.
802
841
 
@@ -1760,3 +1799,275 @@ class _AttributeGroup (object):
1760
1799
  bin.maxval = result.get('maxval', bin.maxval)
1761
1800
  if (bin.minval is None) or (bin.maxval is None):
1762
1801
  raise ValueError('Automatic determination of binning bounds failed.')
1802
+
1803
+ ##
1804
+ ## UTILITIES FOR DENORMALIZATION ##############################################
1805
+ ##
1806
+
1807
+ def _datapath_left_outer_join_by_fkey(path, fk, alias_name=None):
1808
+ """Link a table to the path based on a foreign key reference.
1809
+
1810
+ :param path: a DataPath object
1811
+ :param fk: an ermrest_model.ForeignKey object
1812
+ :param alias_name: an optional 'alias' name to use for the foreign table
1813
+ """
1814
+ assert isinstance(path, DataPath)
1815
+ assert isinstance(fk, _erm.ForeignKey)
1816
+ catalog = path._root._schema._catalog
1817
+
1818
+ # determine 'direction' -- inbound or outbound
1819
+ path_context_table = path.context._base_table._wrapped_table
1820
+ if (path_context_table.schema.name, path_context_table.name) == (fk.table.schema.name, fk.table.name):
1821
+ right = catalog.schemas[fk.pk_table.schema.name].tables[fk.pk_table.name]
1822
+ fkcols = zip(fk.foreign_key_columns, fk.referenced_columns)
1823
+ elif (path_context_table.schema.name, path_context_table.name) == (fk.pk_table.schema.name, fk.pk_table.name):
1824
+ right = catalog.schemas[fk.table.schema.name].tables[fk.table.name]
1825
+ fkcols = zip(fk.referenced_columns, fk.foreign_key_columns)
1826
+ else:
1827
+ raise ValueError('Context table "%s" not referenced by foreign key "%s"' % (path_context_table.name, fk.constraint_name))
1828
+
1829
+ # compose join condition
1830
+ on = None
1831
+ for lcol, rcol in fkcols:
1832
+ lcol = catalog.schemas[lcol.table.schema.name].tables[lcol.table.name].columns[lcol.name]
1833
+ rcol = catalog.schemas[rcol.table.schema.name].tables[rcol.table.name].columns[rcol.name]
1834
+ if on:
1835
+ on = on & (lcol == rcol)
1836
+ else:
1837
+ on = lcol == rcol
1838
+
1839
+ # link
1840
+ path.link(right.alias(alias_name) if alias_name else right, on=on, join_type='left')
1841
+
1842
+
1843
+ def _datapath_deserialize_vizcolumn(path, vizcol, sources=None):
1844
+ """Deserializes a visual column specification.
1845
+
1846
+ If the visible column specifies a foreign key path, the datapath object
1847
+ will be changed by linking the foreign keys in the path.
1848
+
1849
+ :param path: a datapath object
1850
+ :param vizcol: a visible column specification
1851
+ :return: the element to be projected from the datapath or None
1852
+ """
1853
+ assert isinstance(path, DataPath)
1854
+ sources = sources if sources else {}
1855
+ context = path.context
1856
+ table = context._wrapped_table
1857
+ model = table.schema.model
1858
+
1859
+ if isinstance(vizcol, str):
1860
+ # column name specification
1861
+ return context.columns[vizcol]
1862
+ elif isinstance(vizcol, list):
1863
+ # constraint specification
1864
+ try:
1865
+ fk = model.fkey(vizcol)
1866
+ _datapath_left_outer_join_by_fkey(path, fk, alias_name='F')
1867
+ return ArrayD(path.context).alias(path.context._name) # project all attributes
1868
+ except KeyError as e:
1869
+ raise ValueError('Invalid foreign key constraint name: %s. If this is a key constraint name, note that keys are not supported at this time.' % str(e))
1870
+ elif isinstance(vizcol, dict):
1871
+ # resolve visible column
1872
+ while 'sourcekey' in vizcol:
1873
+ temp = sources.get(vizcol['sourcekey'], {})
1874
+ if temp == vizcol:
1875
+ raise ValueError('Visible column self reference for sourcekey "%s"' % vizcol['sourcekey'])
1876
+ vizcol = temp
1877
+ # deserialize source definition
1878
+ source = vizcol.get('source')
1879
+ if not source:
1880
+ # case: none
1881
+ raise ValueError('Could not resolve source definition for visible column')
1882
+ elif isinstance(source, str):
1883
+ # case: column name
1884
+ return context.columns[source]
1885
+ elif isinstance(source, list):
1886
+ # case: path expression
1887
+ # ...validate syntax
1888
+ if not all(isinstance(obj, dict) for obj in source[:-1]):
1889
+ raise ValueError('Source path element must be a foreign key dict')
1890
+ if not isinstance(source[-1], str):
1891
+ raise ValueError('Source path must terminate in a column name string')
1892
+ # link path elements by fkey; and track whether path is outbound only fkeys
1893
+ outbound_only = True
1894
+ for path_elem in source[:-1]:
1895
+ try:
1896
+ fk = model.fkey(path_elem.get('inbound', path_elem.get('outbound')))
1897
+ _datapath_left_outer_join_by_fkey(path, fk, alias_name='F')
1898
+ outbound_only = outbound_only and 'outbound' in path_elem
1899
+ except KeyError as e:
1900
+ raise ValueError('Invalid foreign key constraint name: %s' % str(e))
1901
+ # return terminating column or entity
1902
+ # ...get terminal name
1903
+ terminal = source[-1]
1904
+ # ...get alias name
1905
+ alias = vizcol.get('markdown_name', vizcol.get('name', path.context._name + '_' + terminal))
1906
+ # ...get aggregate function
1907
+ aggregate = {
1908
+ 'min': Min,
1909
+ 'max': Max,
1910
+ 'cnt': Cnt,
1911
+ 'cnd_d': CntD,
1912
+ 'array': Array,
1913
+ 'array_d': ArrayD
1914
+ }.get(vizcol.get('aggregate'), ArrayD)
1915
+ # ...determine projection mode
1916
+ if vizcol.get('entity', True):
1917
+ # case: whole entities
1918
+ return aggregate(path.context).alias(alias)
1919
+ else:
1920
+ # case: specified attribute value(s)
1921
+ if outbound_only:
1922
+ # for outbound only paths, we can project a single value
1923
+ return path.context.columns[terminal].alias(alias)
1924
+ else:
1925
+ # otherwise, we need to use aggregate the values
1926
+ return aggregate(path.context.columns[terminal]).alias(alias)
1927
+ else:
1928
+ raise ValueError('Malformed source: %s' % str(source))
1929
+ else:
1930
+ raise ValueError('Malformed visible column: %s' % str(vizcol))
1931
+
1932
+
1933
+ def _datapath_contextualize(path, context_name='*', context_body=None, groupkey_name='RID'):
1934
+ """Contextualizes a data path to a named visible columns context.
1935
+
1936
+ :param path: a datapath object
1937
+ :param context_name: name of the context within the path's terminating table's "visible columns" annotations
1938
+ :param context_body: a list of visible column definitions, if given, the `context_name` will be ignored
1939
+ :param groupkey_name: column name for the group by key of the generated query expression (default: 'RID')
1940
+ :return: a 'contextualized' attribute group query object
1941
+ """
1942
+ assert isinstance(path, DataPath)
1943
+ path = copy.deepcopy(path)
1944
+ context = path.context
1945
+ table = context._wrapped_table
1946
+ sources = table.annotations.get(_erm.tag.source_definitions, {}).get('sources')
1947
+ vizcols = context_body if context_body else table.annotations.get(_erm.tag.visible_columns, {}).get(context_name, [])
1948
+ if not vizcols:
1949
+ raise ValueError('Visible columns context "%s" not found for table %s:%s' % (context_name, table.schema.name, table.name))
1950
+ groupkey = context.columns[groupkey_name]
1951
+ projection = []
1952
+
1953
+ for vizcol in vizcols:
1954
+ try:
1955
+ projection.append(_datapath_deserialize_vizcolumn(path, vizcol, sources=sources))
1956
+ path.context = context
1957
+ except ValueError as e:
1958
+ logger.warning(str(e))
1959
+
1960
+ def not_same_as_group_key(x):
1961
+ assert isinstance(groupkey, _ColumnWrapper)
1962
+ if not isinstance(x, _ColumnWrapper):
1963
+ return True
1964
+ return groupkey._wrapped_column != x._wrapped_column
1965
+
1966
+ projection = filter(not_same_as_group_key, projection) # project groupkey only once
1967
+ query = path.groupby(groupkey).attributes(*projection)
1968
+ return query
1969
+
1970
+
1971
+ def _datapath_generate_simple_denormalization(path, include_whole_entities=False):
1972
+ """Generates a denormalized form of the table expressed in a visible columns specification.
1973
+
1974
+ :param path: a datapath object
1975
+ :param include_whole_entities: if a denormalization cannot find a 'name' like terminal, include the whole entity (i.e., all attributes), else return just the 'RID'
1976
+ :return: a generated visible columns specification based on a denormalization heuristic
1977
+ """
1978
+ assert isinstance(path, DataPath)
1979
+ context = path.context
1980
+ table = context._wrapped_table
1981
+
1982
+ fkeys = list(table.foreign_keys)
1983
+ single_column_fkeys = {
1984
+ fkey.foreign_key_columns[0].name: fkey
1985
+ for fkey in table.foreign_keys if len(fkey.foreign_key_columns) == 1
1986
+ }
1987
+
1988
+ def _fkey_to_vizcol(name, fk, inbound=None):
1989
+ # name columns to look for in related tables
1990
+ name_candidates = [
1991
+ 'displayname',
1992
+ 'preferredname',
1993
+ 'fullname',
1994
+ 'name',
1995
+ 'title',
1996
+ 'label'
1997
+ ]
1998
+
1999
+ # determine terminal column
2000
+ terminal = 'RID'
2001
+ for candidate_col in fk.pk_table.columns:
2002
+ if candidate_col.name.lower().replace(' ', '').replace('_', '') in name_candidates:
2003
+ terminal = candidate_col.name
2004
+ break
2005
+
2006
+ # define source path
2007
+ source = [{'outbound': fk.names[0]}, terminal]
2008
+ if inbound:
2009
+ source = [{'inbound': inbound.names[0]}] + source
2010
+
2011
+ # return vizcol spec
2012
+ return {
2013
+ 'markdown_name': name,
2014
+ 'source': source,
2015
+ 'entity': include_whole_entities and terminal == 'RID'
2016
+ }
2017
+
2018
+ # assemble the visible column:
2019
+ # 1. column or single column fkeys
2020
+ # 2. all other (outbound fkey) related tables
2021
+ # 3. all associated tables
2022
+ vizcols = []
2023
+ for col in table.column_definitions:
2024
+ if col.name in single_column_fkeys:
2025
+ fkey = single_column_fkeys[col.name]
2026
+ vizcols.append(_fkey_to_vizcol(col.name, fkey))
2027
+ del single_column_fkeys[col.name]
2028
+ fkeys.remove(fkey)
2029
+ else:
2030
+ vizcols.append(col.name)
2031
+
2032
+ for outbound_fkey in fkeys:
2033
+ vizcols.append(_fkey_to_vizcol(outbound_fkey.constraint_name, outbound_fkey))
2034
+
2035
+ for inbound_fkey in table.referenced_by:
2036
+ if inbound_fkey.table.is_association():
2037
+ vizcols.append(
2038
+ _fkey_to_vizcol(
2039
+ inbound_fkey.table.name,
2040
+ inbound_fkey.table.foreign_keys[0] if inbound_fkey != inbound_fkey.table.foreign_keys[0] else inbound_fkey.table.foreign_keys[1],
2041
+ inbound=inbound_fkey
2042
+ )
2043
+ )
2044
+
2045
+ return vizcols
2046
+
2047
+ def simple_denormalization(path):
2048
+ """A simple heuristic denormalization."""
2049
+ return _datapath_generate_simple_denormalization(path)
2050
+
2051
+ def simple_denormalization_with_whole_entities(path):
2052
+ """A simple heuristic denormalization with related and associated entities."""
2053
+ return _datapath_generate_simple_denormalization(path, include_whole_entities=True)
2054
+
2055
+ def _datapath_denormalize(path, context_name=None, heuristic=None, groupkey_name='RID'):
2056
+ """Denormalizes a path based on annotations or heuristics.
2057
+
2058
+ :param path: a DataPath object
2059
+ :param context_name: name of the visible-columns context or if none given, will attempt apply heuristics
2060
+ :param heuristic: heuristic to apply if no context name specified
2061
+ :param groupkey_name: column name for the group by key of the generated query expression (default: 'RID')
2062
+ """
2063
+ assert isinstance(path, DataPath)
2064
+ assert context_name is None or isinstance(context_name, str)
2065
+ assert isinstance(groupkey_name, str)
2066
+ heuristic = heuristic or simple_denormalization
2067
+ assert callable(heuristic)
2068
+ return _datapath_contextualize(
2069
+ path,
2070
+ context_name=context_name,
2071
+ context_body=None if context_name else heuristic(path),
2072
+ groupkey_name=groupkey_name
2073
+ )
@@ -1,9 +1,11 @@
1
1
  from deriva.transfer.download.deriva_download import DerivaDownload, GenericDownloader, DerivaDownloadError, \
2
- DerivaDownloadConfigurationError, DerivaDownloadAuthenticationError, DerivaDownloadAuthorizationError
2
+ DerivaDownloadConfigurationError, DerivaDownloadAuthenticationError, DerivaDownloadAuthorizationError, \
3
+ DerivaDownloadBaggingError
3
4
  from deriva.transfer.download.deriva_download_cli import DerivaDownloadCLI
4
5
 
5
6
  from deriva.transfer.upload.deriva_upload import DerivaUpload, GenericUploader, DerivaUploadError, DerivaUploadError, \
6
- DerivaUploadConfigurationError, DerivaUploadCatalogCreateError, DerivaUploadCatalogUpdateError
7
+ DerivaUploadConfigurationError, DerivaUploadCatalogCreateError, DerivaUploadCatalogUpdateError, \
8
+ DerivaUploadAuthenticationError
7
9
  from deriva.transfer.upload.deriva_upload_cli import DerivaUploadCLI
8
10
 
9
11
  from deriva.transfer.backup.deriva_backup import DerivaBackup, DerivaBackupAuthenticationError, \
@@ -16,3 +16,7 @@ class DerivaDownloadAuthorizationError(Exception):
16
16
 
17
17
  class DerivaDownloadTimeoutError(Exception):
18
18
  pass
19
+
20
+
21
+ class DerivaDownloadBaggingError(Exception):
22
+ pass