gooddata-pipelines 1.74.1.dev1__tar.gz → 1.74.1.dev3__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 (160) hide show
  1. gooddata_pipelines-1.74.1.dev3/AGENTS.md +96 -0
  2. gooddata_pipelines-1.74.1.dev3/CLAUDE.md +1 -0
  3. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/PKG-INFO +12 -9
  4. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/README.md +10 -7
  5. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/pyproject.toml +4 -8
  6. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/api/gooddata_api.py +13 -39
  7. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/api/gooddata_api_wrapper.py +1 -3
  8. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/backup_input_processor.py +10 -19
  9. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/backup_manager.py +16 -44
  10. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/base_manager.py +7 -9
  11. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/constants.py +1 -5
  12. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/csv_reader.py +2 -6
  13. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/models/storage.py +10 -25
  14. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/restore_manager.py +29 -82
  15. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/storage/azure_storage.py +16 -44
  16. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/storage/base_storage.py +2 -6
  17. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/storage/local_storage.py +3 -9
  18. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/storage/s3_storage.py +8 -23
  19. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/input_processor.py +19 -70
  20. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/input_validator.py +16 -29
  21. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/ldm_extension_manager.py +25 -68
  22. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/models/custom_data_object.py +6 -20
  23. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/user_data_filters/user_data_filters.py +22 -67
  24. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/models/permissions.py +15 -48
  25. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/models/user_groups.py +1 -3
  26. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/permissions.py +15 -47
  27. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/user_groups.py +21 -59
  28. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/users.py +11 -29
  29. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/models.py +1 -3
  30. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/workspace.py +27 -79
  31. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_filters.py +21 -54
  32. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_parser.py +11 -33
  33. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_validator.py +15 -35
  34. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/generic/config.py +4 -9
  35. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/provisioning.py +11 -31
  36. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/utils/exceptions.py +13 -39
  37. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/utils/utils.py +5 -15
  38. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/utils/decorators.py +1 -3
  39. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/utils/file_utils.py +2 -2
  40. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/utils/rate_limiter.py +4 -6
  41. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/backup_and_restore/test_backup.py +16 -45
  42. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/backup_and_restore/test_backup_input_processor.py +7 -22
  43. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/backup_and_restore/test_restore.py +23 -68
  44. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/conftest.py +2 -3
  45. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/panther/test_api_wrapper.py +0 -1
  46. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/panther/test_sdk_wrapper.py +4 -12
  47. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/users/test_permissions.py +38 -77
  48. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/users/test_user_groups.py +4 -13
  49. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/users/test_users.py +38 -62
  50. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/test_workspace.py +3 -4
  51. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/test_workspace_data_filters.py +18 -44
  52. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/test_workspace_data_parser.py +3 -4
  53. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/test_workspace_data_validator.py +6 -21
  54. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/test_provisioning.py +3 -5
  55. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/conftest.py +0 -1
  56. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_input_processor.py +9 -26
  57. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_input_validator.py +1 -4
  58. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_ldm_extension_manager.py +37 -97
  59. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_merge_ldm.py +12 -23
  60. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_models/test_analytical_object.py +3 -5
  61. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_models/test_custom_data_object.py +8 -24
  62. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/utils/test_decorators.py +1 -4
  63. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/utils/test_rate_limiter.py +3 -4
  64. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/.gitignore +0 -0
  65. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/LICENSE.txt +0 -0
  66. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/Makefile +0 -0
  67. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/__init__.py +0 -0
  68. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/_version.py +0 -0
  69. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/api/__init__.py +0 -0
  70. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/__init__.py +0 -0
  71. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/models/__init__.py +0 -0
  72. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/models/input_type.py +0 -0
  73. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/models/workspace_response.py +0 -0
  74. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/storage/__init__.py +0 -0
  75. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/__init__.py +0 -0
  76. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/models/__init__.py +0 -0
  77. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/models/aliases.py +0 -0
  78. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/models/analytical_object.py +0 -0
  79. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/logger/__init__.py +0 -0
  80. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/logger/logger.py +0 -0
  81. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/__init__.py +0 -0
  82. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/assets/wdf_setting.json +0 -0
  83. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/__init__.py +0 -0
  84. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/user_data_filters/__init__.py +0 -0
  85. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/user_data_filters/models/__init__.py +0 -0
  86. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/user_data_filters/models/udf_models.py +0 -0
  87. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/__init__.py +0 -0
  88. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/models/__init__.py +0 -0
  89. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/models/users.py +0 -0
  90. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/__init__.py +0 -0
  91. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/generic/__init__.py +0 -0
  92. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/generic/provision.py +0 -0
  93. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/utils/__init__.py +0 -0
  94. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/utils/context_objects.py +0 -0
  95. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/py.typed +0 -0
  96. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/utils/__init__.py +0 -0
  97. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/__init__.py +0 -0
  98. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/backup_and_restore/__init__.py +0 -0
  99. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/__init__.py +0 -0
  100. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/__init__.py +0 -0
  101. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_conf.yaml +0 -0
  102. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/analytical_dashboard_extensions/.gitkeep +0 -0
  103. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/analytical_dashboards/.gitkeep +0 -0
  104. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/dashboard_plugins/.gitkeep +0 -0
  105. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/filter_contexts/.gitkeep +0 -0
  106. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/metrics/.gitkeep +0 -0
  107. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/visualization_objects/.gitkeep +0 -0
  108. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/ldm/datasets/test.yaml +0 -0
  109. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/ldm/date_instances/testinstance.yaml +0 -0
  110. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/analytical_dashboard_extensions/.gitkeep +0 -0
  111. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/analytical_dashboards/id.yaml +0 -0
  112. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/dashboard_plugins/.gitkeep +0 -0
  113. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/filter_contexts/id.yaml +0 -0
  114. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/metrics/.gitkeep +0 -0
  115. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/visualization_objects/test.yaml +0 -0
  116. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/ldm/datasets/.gitkeep +0 -0
  117. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/ldm/date_instances/.gitkeep +0 -0
  118. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/analytical_dashboard_extensions/.gitkeep +0 -0
  119. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/analytical_dashboards/.gitkeep +0 -0
  120. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/dashboard_plugins/.gitkeep +0 -0
  121. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/filter_contexts/.gitkeep +0 -0
  122. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/metrics/.gitkeep +0 -0
  123. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/visualization_objects/.gitkeep +0 -0
  124. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/ldm/datasets/.gitkeep +0 -0
  125. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/ldm/date_instances/.gitkeep +0 -0
  126. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/user_data_filters/.gitkeep +0 -0
  127. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_local_conf.yaml +0 -0
  128. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/custom_fields/response_get_all_dashboards.json +0 -0
  129. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/custom_fields/response_get_all_metrics.json +0 -0
  130. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/custom_fields/response_get_all_visualizations.json +0 -0
  131. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/mock_responses.py +0 -0
  132. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/profiles.yaml +0 -0
  133. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/permissions/existing_upstream_permissions.json +0 -0
  134. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/permissions/permissions_expected_full_load.json +0 -0
  135. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/permissions/permissions_expected_incremental_load.json +0 -0
  136. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/permissions/permissions_input_full_load.json +0 -0
  137. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/permissions/permissions_input_incremental_load.json +0 -0
  138. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/existing_upstream_users.json +0 -0
  139. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/profile_response_content.json +0 -0
  140. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_expected_full_load.json +0 -0
  141. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_expected_incremental_load.json +0 -0
  142. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_input_full_load.json +0 -0
  143. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_input_full_load_modifies_protected_user.json +0 -0
  144. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_input_incremental_load.json +0 -0
  145. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_input_incremental_load_deletes_protected_user.json +0 -0
  146. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_input_incremental_load_modifies_protected_user.json +0 -0
  147. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/restore/test_conf.yaml +0 -0
  148. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/restore/test_udf_root/filter1.yaml +0 -0
  149. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/restore/test_udf_root/filter2.yaml +0 -0
  150. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/restore/test_udf_root/user_data_filters/filter1.yaml +0 -0
  151. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/restore/test_udf_root/user_data_filters/filter2.yaml +0 -0
  152. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/panther/__init__.py +0 -0
  153. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/__init__.py +0 -0
  154. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/__init__.py +0 -0
  155. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/users/__init__.py +0 -0
  156. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/__init__.py +0 -0
  157. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/test_provisioning.py +0 -0
  158. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/__init__.py +0 -0
  159. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_models/__init__.py +0 -0
  160. {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tox.ini +0 -0
@@ -0,0 +1,96 @@
1
+ # gooddata-pipelines
2
+
3
+ A lifecycle-automation library on top of `gooddata-sdk`, covering three independent areas:
4
+ provisioning users, groups, permissions and workspaces; backing up and restoring workspace
5
+ metadata to local disk, S3 or Azure Blob; and extending an existing workspace's logical
6
+ data model with custom datasets and fields. Each area is entered through a small set of
7
+ manager or provisioner classes.
8
+
9
+ ## Owns
10
+
11
+ - Provisioning flows for users, user groups, permissions, user data filters, and workspace
12
+ hierarchies, with pydantic-validated inputs
13
+ - Backup and restore of workspace metadata across local, S3 and Azure Blob targets
14
+ - LDM extension for child workspaces
15
+
16
+ ## Does NOT Own
17
+
18
+ - Core platform API and service abstractions → `gooddata-sdk`
19
+ - dbt metadata conversion → `gooddata-dbt`
20
+ - Flight RPC runtime → `gooddata-flight-server`
21
+
22
+ ## Architecture
23
+
24
+ | Module | Role |
25
+ |---|---|
26
+ | `provisioning/` | `UserProvisioner`, `UserGroupProvisioner`, `PermissionProvisioner`, `WorkspaceProvisioner`, `UserDataFilterProvisioner` |
27
+ | `backup_and_restore/` | `BackupManager`, `RestoreManager`, plus `storage/` backends |
28
+ | `ldm_extension/` | `LdmExtensionManager` |
29
+ | `api/`, `utils/`, `logger/` | supporting HTTP, helpers and the log observer |
30
+
31
+ **Depends on**: `gooddata-sdk`, `pydantic`, `requests`, `boto3`, `azure-storage-blob`,
32
+ `azure-identity`.
33
+
34
+ ### Construction
35
+
36
+ Managers and provisioners are built through classmethods, not `__init__`:
37
+
38
+ ```python
39
+ provisioner = UserProvisioner.create(host, token)
40
+ # or
41
+ provisioner = UserProvisioner.create_from_profile(profile="default")
42
+ ```
43
+
44
+ Each exposes a `.logger` (`LogObserver`) that a stdlib logger can `.subscribe()` to.
45
+
46
+ Provisioning is also reachable generically via `provision(data, workflow_type, host,
47
+ token)` driven by `WorkflowType`, intended for config- or orchestration-driven callers.
48
+
49
+ ## Gotchas
50
+
51
+ **`full_load()` deletes.** Full load treats the list you pass as the complete desired
52
+ state: it diffs your input against what exists upstream and deletes everything not present
53
+ (`ids_to_delete = panther_id.difference(source_id)`). `incremental_load()` instead applies
54
+ only the explicit create/update/delete entries you provide and leaves everything else
55
+ alone. Confusing the two is the easiest way to write accidentally destructive code in this
56
+ repo — when in doubt, use incremental.
57
+
58
+ **`attrs` and `pyyaml` are used but not declared.** Both are imported throughout this
59
+ package yet appear in neither its dependencies; they arrive transitively through
60
+ `gooddata-sdk`. Fine today, silently broken if the SDK's dependency set ever changes.
61
+
62
+ **Backups are zip archives with a fixed internal layout.** A backup is written per
63
+ organization and workspace as `gooddata_layouts.zip`, containing the declarative layout
64
+ plus `user_data_filters/`, `filter_views/` and `automations/`. Restore expects exactly that
65
+ shape, so anything that changes the archive layout breaks restore of existing backups.
66
+
67
+ **LDM extension is additive, not a general LDM editor.** `LdmExtensionManager.process()`
68
+ adds custom datasets and fields validated through `CustomDatasetDefinition` /
69
+ `CustomFieldDefinition`, optionally merging into the existing LDM. It can also prune
70
+ previously-managed datasets carrying its `management_tag` when they are absent from the
71
+ current call. It has an optional relations-integrity check that reverts the change if it
72
+ would break more references than it fixes — do not remove that safety net casually.
73
+
74
+ **Backup batching, rate limiting and retry are configurable and load-bearing.**
75
+ `BackupRestoreConfig` carries `batch_size` and `api_calls_per_second`; a failed batch is
76
+ retried with exponential backoff up to `BackupSettings.MAX_RETRIES` before the whole run
77
+ aborts. These are the knobs for backup reliability and speed.
78
+
79
+ **`UserDataFilterProvisioner` is not wired into the generic path.** It is provisioned
80
+ directly rather than through `PROVISIONING_CONFIG` / `WorkflowType`, so do not assume
81
+ feature parity across provisioners.
82
+
83
+ **CSV input is backup-only.** `CSVReader.read_backup_csv` (a single-column list of
84
+ workspace ids) serves `BackupManager` only. Provisioning takes validated pydantic model
85
+ lists that the caller builds from whatever source they like.
86
+
87
+ **Input models forbid unknown keys and want real lists.** `UserFullLoad` and friends set
88
+ `extra="forbid"`, so a stray CSV column raises `ValidationError`, and `user_groups` is a
89
+ `list[str]` — a delimited CSV cell has to be split before validating. The models' only
90
+ alternate constructor is `from_sdk_obj`; there is no `from_list_of_dicts` helper, so the
91
+ caller builds the list. The README shows the working shape.
92
+
93
+ ## Testing
94
+
95
+ Package-local pytest suites under `tests/`. Cloud storage targets are exercised with
96
+ `moto` rather than live buckets.
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gooddata-pipelines
3
- Version: 1.74.1.dev1
3
+ Version: 1.74.1.dev3
4
4
  Summary: GoodData Cloud lifecycle automation pipelines
5
5
  Author-email: GoodData <support@gooddata.com>
6
6
  License-Expression: MIT
@@ -10,7 +10,7 @@ Requires-Dist: azure-identity<2.0.0,>=1.15.0
10
10
  Requires-Dist: azure-storage-blob<13.0.0,>=12.19.0
11
11
  Requires-Dist: boto3-stubs<2.0.0,>=1.39.3
12
12
  Requires-Dist: boto3<2.0.0,>=1.39.3
13
- Requires-Dist: gooddata-sdk~=1.74.1.dev1
13
+ Requires-Dist: gooddata-sdk~=1.74.1.dev3
14
14
  Requires-Dist: pydantic<3.0.0,>=2.9.2
15
15
  Requires-Dist: requests<3.0.0,>=2.32.3
16
16
  Requires-Dist: types-pyyaml<7.0.0,>=6.0.12.20250326
@@ -56,9 +56,10 @@ from pathlib import Path
56
56
  # Import the Entity Provisioner class and corresponding model from the gooddata_pipelines library
57
57
  from gooddata_pipelines import UserFullLoad, UserProvisioner
58
58
 
59
- # Create the Provisioner instance - you can also create the instance from a GDC yaml profile
60
- provisioner = UserProvisioner(
61
- host=os.environ["GDC_HOSTNAME"], token=os.environ["GDC_AUTH_TOKEN"]
59
+ # Create the Provisioner instance - you can also create the instance from a GDC yaml
60
+ # profile with UserProvisioner.create_from_profile(profile="default")
61
+ provisioner = UserProvisioner.create(
62
+ os.environ["GDC_HOSTNAME"], os.environ["GDC_AUTH_TOKEN"]
62
63
  )
63
64
 
64
65
  # Optional: set up logging and subscribe to logs emitted by the provisioner
@@ -71,10 +72,12 @@ source_data_path: Path = Path("path/to/some.csv")
71
72
  source_data_reader = DictReader(source_data_path.read_text().splitlines())
72
73
  source_data = [row for row in source_data_reader]
73
74
 
74
- # Validate your input data
75
- full_load_data: list[UserFullLoad] = UserFullLoad.from_list_of_dicts(
76
- source_data
77
- )
75
+ # Validate your input data. UserFullLoad forbids unknown keys, and user_groups must be a
76
+ # list, so split the delimited CSV column before validating.
77
+ full_load_data: list[UserFullLoad] = [
78
+ UserFullLoad(**{**row, "user_groups": row["user_groups"].split("|")})
79
+ for row in source_data
80
+ ]
78
81
 
79
82
  # Run the provisioning
80
83
  provisioner.full_load(full_load_data)
@@ -37,9 +37,10 @@ from pathlib import Path
37
37
  # Import the Entity Provisioner class and corresponding model from the gooddata_pipelines library
38
38
  from gooddata_pipelines import UserFullLoad, UserProvisioner
39
39
 
40
- # Create the Provisioner instance - you can also create the instance from a GDC yaml profile
41
- provisioner = UserProvisioner(
42
- host=os.environ["GDC_HOSTNAME"], token=os.environ["GDC_AUTH_TOKEN"]
40
+ # Create the Provisioner instance - you can also create the instance from a GDC yaml
41
+ # profile with UserProvisioner.create_from_profile(profile="default")
42
+ provisioner = UserProvisioner.create(
43
+ os.environ["GDC_HOSTNAME"], os.environ["GDC_AUTH_TOKEN"]
43
44
  )
44
45
 
45
46
  # Optional: set up logging and subscribe to logs emitted by the provisioner
@@ -52,10 +53,12 @@ source_data_path: Path = Path("path/to/some.csv")
52
53
  source_data_reader = DictReader(source_data_path.read_text().splitlines())
53
54
  source_data = [row for row in source_data_reader]
54
55
 
55
- # Validate your input data
56
- full_load_data: list[UserFullLoad] = UserFullLoad.from_list_of_dicts(
57
- source_data
58
- )
56
+ # Validate your input data. UserFullLoad forbids unknown keys, and user_groups must be a
57
+ # list, so split the delimited CSV column before validating.
58
+ full_load_data: list[UserFullLoad] = [
59
+ UserFullLoad(**{**row, "user_groups": row["user_groups"].split("|")})
60
+ for row in source_data
61
+ ]
59
62
 
60
63
  # Run the provisioning
61
64
  provisioner.full_load(full_load_data)
@@ -1,7 +1,7 @@
1
1
  # (C) 2025 GoodData Corporation
2
2
  [project]
3
3
  name = "gooddata-pipelines"
4
- version = "1.74.1.dev1"
4
+ version = "1.74.1.dev3"
5
5
  description = "GoodData Cloud lifecycle automation pipelines"
6
6
  authors = [{ name = "GoodData", email = "support@gooddata.com" }]
7
7
  license = "MIT"
@@ -11,7 +11,7 @@ dependencies = [
11
11
  "pydantic (>=2.9.2,<3.0.0)",
12
12
  "requests (>=2.32.3,<3.0.0)",
13
13
  "types-requests (>1.0.0,<3.0.0)",
14
- "gooddata-sdk~=1.74.1.dev1",
14
+ "gooddata-sdk~=1.74.1.dev3",
15
15
  "boto3 (>=1.39.3,<2.0.0)",
16
16
  "boto3-stubs (>=1.39.3,<2.0.0)",
17
17
  "azure-storage-blob (>=12.19.0,<13.0.0)",
@@ -19,14 +19,10 @@ dependencies = [
19
19
  "types-pyyaml (>=6.0.12.20250326,<7.0.0)",
20
20
  ]
21
21
 
22
- [tool.ruff]
23
- exclude = [".venv"]
24
- line-length = 80
25
-
26
22
  [dependency-groups]
27
23
  test = [
28
- "pytest~=8.3.4",
29
- "pytest-cov~=6.0.0",
24
+ "pytest~=9.1.1",
25
+ "pytest-cov~=7.1.0",
30
26
  "pytest-json-report==1.5.0",
31
27
  "pytest-mock>=3.14.0,<4.0.0",
32
28
  "moto>=5.1.6,<6.0.0",
@@ -38,9 +38,7 @@ class ApiMethods:
38
38
  """
39
39
  return f"{self.base_url}{endpoint}"
40
40
 
41
- def get_all_workspace_data_filters(
42
- self, workspace_id: str
43
- ) -> requests.Response:
41
+ def get_all_workspace_data_filters(self, workspace_id: str) -> requests.Response:
44
42
  """Gets all workspace data filters for a given workspace.
45
43
 
46
44
  Args:
@@ -52,9 +50,7 @@ class ApiMethods:
52
50
  url = f"/entities/workspaces/{workspace_id}/workspaceDataFilters"
53
51
  return self._get(url)
54
52
 
55
- def get_workspace_data_filter_settings(
56
- self, workspace_id: str
57
- ) -> requests.Response:
53
+ def get_workspace_data_filter_settings(self, workspace_id: str) -> requests.Response:
58
54
  """Gets all workspace data filter settings for a given workspace.
59
55
 
60
56
  Args:
@@ -66,9 +62,7 @@ class ApiMethods:
66
62
  url = f"/entities/workspaces/{workspace_id}/workspaceDataFilterSettings?include=workspaceDataFilters"
67
63
  return self._get(url)
68
64
 
69
- def get_workspace_data_filter_setting(
70
- self, workspace_id: str, wdf_id: str
71
- ) -> requests.Response:
65
+ def get_workspace_data_filter_setting(self, workspace_id: str, wdf_id: str) -> requests.Response:
72
66
  """Gets a specific workspace data filter setting.
73
67
 
74
68
  Args:
@@ -119,9 +113,7 @@ class ApiMethods:
119
113
  requests.Response: The response from the server containing the
120
114
  created workspace data filter setting.
121
115
  """
122
- endpoint = (
123
- f"/entities/workspaces/{workspace_id}/workspaceDataFilterSettings/"
124
- )
116
+ endpoint = f"/entities/workspaces/{workspace_id}/workspaceDataFilterSettings/"
125
117
  return self._post(
126
118
  endpoint,
127
119
  wdf_setting,
@@ -153,9 +145,7 @@ class ApiMethods:
153
145
  endpoint = f"/layout/workspaces/{workspace_id}/userDataFilters"
154
146
  return self._get(endpoint)
155
147
 
156
- def put_user_data_filters(
157
- self, workspace_id: str, user_data_filters: dict[str, Any]
158
- ) -> requests.Response:
148
+ def put_user_data_filters(self, workspace_id: str, user_data_filters: dict[str, Any]) -> requests.Response:
159
149
  """Puts the user data filters into GoodData workspace."""
160
150
  headers = {**self.headers, "Content-Type": "application/json"}
161
151
  return self._put(
@@ -166,25 +156,17 @@ class ApiMethods:
166
156
 
167
157
  def get_automations(self, workspace_id: str) -> requests.Response:
168
158
  """Gets the automations for a given workspace."""
169
- endpoint = (
170
- f"/entities/workspaces/{workspace_id}/automations?include=ALL"
171
- )
159
+ endpoint = f"/entities/workspaces/{workspace_id}/automations?include=ALL"
172
160
  return self._get(endpoint)
173
161
 
174
- def post_automation(
175
- self, workspace_id: str, automation: dict[str, Any]
176
- ) -> requests.Response:
162
+ def post_automation(self, workspace_id: str, automation: dict[str, Any]) -> requests.Response:
177
163
  """Posts an automation for a given workspace."""
178
164
  endpoint = f"/entities/workspaces/{workspace_id}/automations"
179
165
  return self._post(endpoint, automation)
180
166
 
181
- def delete_automation(
182
- self, workspace_id: str, automation_id: str
183
- ) -> requests.Response:
167
+ def delete_automation(self, workspace_id: str, automation_id: str) -> requests.Response:
184
168
  """Deletes an automation for a given workspace."""
185
- endpoint = (
186
- f"/entities/workspaces/{workspace_id}/automations/{automation_id}"
187
- )
169
+ endpoint = f"/entities/workspaces/{workspace_id}/automations/{automation_id}"
188
170
  return self._delete(endpoint)
189
171
 
190
172
  def get_all_metrics(self, workspace_id: str) -> requests.Response:
@@ -199,9 +181,7 @@ class ApiMethods:
199
181
  headers = {**self.headers, "X-GDC-VALIDATE-RELATIONS": "true"}
200
182
  return self._get(endpoint, headers=headers)
201
183
 
202
- def get_all_visualization_objects(
203
- self, workspace_id: str
204
- ) -> requests.Response:
184
+ def get_all_visualization_objects(self, workspace_id: str) -> requests.Response:
205
185
  """Get all visualizations from the specified workspace.
206
186
 
207
187
  Args:
@@ -230,9 +210,7 @@ class ApiMethods:
230
210
  endpoint = "/profile"
231
211
  return self._get(endpoint)
232
212
 
233
- def _get(
234
- self, endpoint: str, headers: dict[str, str] | None = None
235
- ) -> requests.Response:
213
+ def _get(self, endpoint: str, headers: dict[str, str] | None = None) -> requests.Response:
236
214
  """Sends a GET request to the server.
237
215
 
238
216
  Args:
@@ -267,9 +245,7 @@ class ApiMethods:
267
245
  request_headers = headers if headers else self.headers
268
246
  data_json = json.dumps(data)
269
247
 
270
- return requests.post(
271
- url, data=data_json, headers=request_headers, timeout=TIMEOUT
272
- )
248
+ return requests.post(url, data=data_json, headers=request_headers, timeout=TIMEOUT)
273
249
 
274
250
  def _put(
275
251
  self,
@@ -291,9 +267,7 @@ class ApiMethods:
291
267
  request_headers = headers if headers else self.headers
292
268
  data_json = json.dumps(data)
293
269
 
294
- return requests.put(
295
- url, data=data_json, headers=request_headers, timeout=TIMEOUT
296
- )
270
+ return requests.put(url, data=data_json, headers=request_headers, timeout=TIMEOUT)
297
271
 
298
272
  def _delete(
299
273
  self,
@@ -47,9 +47,7 @@ class GoodDataApi(ApiMethods):
47
47
  raise RuntimeError("Host is not set. Please provide a valid host.")
48
48
 
49
49
  if host == "":
50
- raise ValueError(
51
- "Host is an empty string. Please provide a valid host."
52
- )
50
+ raise ValueError("Host is an empty string. Please provide a valid host.")
53
51
 
54
52
  # Remove trailing slash if present.
55
53
  if host[-1] == "/":
@@ -43,7 +43,9 @@ class BackupInputProcessor:
43
43
  + "filter=parent.id=={parent_id}"
44
44
  + f"&include=parent&page=0&size={self.page_size}&sort=name,asc&metaInclude=page,hierarchy"
45
45
  )
46
- self.all_workspaces_endpoint = f"{self.base_workspace_endpoint}?page=0&size={self.page_size}&sort=name,asc&metaInclude=page"
46
+ self.all_workspaces_endpoint = (
47
+ f"{self.base_workspace_endpoint}?page=0&size={self.page_size}&sort=name,asc&metaInclude=page"
48
+ )
47
49
 
48
50
  @attrs.define
49
51
  class _ProcessDataOutput:
@@ -59,9 +61,7 @@ class BackupInputProcessor:
59
61
  if response.ok:
60
62
  return WorkspaceResponse(**response.json())
61
63
  else:
62
- raise RuntimeError(
63
- f"Failed to fetch data from the API. URL: {endpoint}"
64
- )
64
+ raise RuntimeError(f"Failed to fetch data from the API. URL: {endpoint}")
65
65
 
66
66
  @staticmethod
67
67
  def process_data(data: list[Workspace]) -> _ProcessDataOutput:
@@ -74,9 +74,8 @@ class BackupInputProcessor:
74
74
  children.append(workspace.id)
75
75
 
76
76
  # if hierarchy is present and has children, append child workspace ID to sub_parents
77
- if workspace.meta and workspace.meta.hierarchy:
78
- if workspace.meta.hierarchy.children_count > 0:
79
- sub_parents.append(workspace.id)
77
+ if workspace.meta and workspace.meta.hierarchy and workspace.meta.hierarchy.children_count > 0:
78
+ sub_parents.append(workspace.id)
80
79
  return BackupInputProcessor._ProcessDataOutput(children, sub_parents)
81
80
 
82
81
  def log_paging_progress(self, response: WorkspaceResponse) -> None:
@@ -94,9 +93,7 @@ class BackupInputProcessor:
94
93
  if current_page and total_pages:
95
94
  self.logger.info(f"Fetched page: {current_page} of {total_pages}")
96
95
 
97
- def _paginate(
98
- self, url: str | None
99
- ) -> list["BackupInputProcessor._ProcessDataOutput"]:
96
+ def _paginate(self, url: str | None) -> list["BackupInputProcessor._ProcessDataOutput"]:
100
97
  """Paginates through the API responses and returns a list of workspace IDs."""
101
98
  result: list[BackupInputProcessor._ProcessDataOutput] = []
102
99
  while url:
@@ -114,9 +111,7 @@ class BackupInputProcessor:
114
111
 
115
112
  all_children, sub_parents = [], []
116
113
 
117
- results: list[BackupInputProcessor._ProcessDataOutput] = self._paginate(
118
- url
119
- )
114
+ results: list[BackupInputProcessor._ProcessDataOutput] = self._paginate(url)
120
115
 
121
116
  for result in results:
122
117
  all_children.extend(result.workspace_ids)
@@ -127,9 +122,7 @@ class BackupInputProcessor:
127
122
  all_children += self.get_hierarchy(subparent)
128
123
 
129
124
  if not all_children:
130
- self.logger.warning(
131
- f"No child workspaces found for parent workspace ID: {parent_id}"
132
- )
125
+ self.logger.warning(f"No child workspaces found for parent workspace ID: {parent_id}")
133
126
 
134
127
  return all_children
135
128
 
@@ -144,9 +137,7 @@ class BackupInputProcessor:
144
137
 
145
138
  all_workspaces: list[str] = []
146
139
 
147
- results: list[BackupInputProcessor._ProcessDataOutput] = self._paginate(
148
- url
149
- )
140
+ results: list[BackupInputProcessor._ProcessDataOutput] = self._paginate(url)
150
141
 
151
142
  for result in results:
152
143
  all_workspaces.extend(result.workspace_ids)
@@ -95,15 +95,12 @@ class BackupManager(BaseManager):
95
95
  def _get_automations_from_api(self, workspace_id: str) -> Any:
96
96
  """Returns automations for the workspace as JSON."""
97
97
  with self._api_rate_limiter:
98
- response: requests.Response = self._api.get_automations(
99
- workspace_id
100
- )
98
+ response: requests.Response = self._api.get_automations(workspace_id)
101
99
  if response.ok:
102
100
  return response.json()
103
101
  else:
104
102
  raise RuntimeError(
105
- f"Failed to get automations for {workspace_id}. "
106
- + f"{response.status_code}: {response.text}"
103
+ f"Failed to get automations for {workspace_id}. " + f"{response.status_code}: {response.text}"
107
104
  )
108
105
 
109
106
  def _store_automations(self, export_path: Path, workspace_id: str) -> None:
@@ -120,9 +117,7 @@ class BackupManager(BaseManager):
120
117
  "automations",
121
118
  )
122
119
 
123
- automations_file_path: Path = Path(
124
- automations_folder_path, "automations.json"
125
- )
120
+ automations_file_path: Path = Path(automations_folder_path, "automations.json")
126
121
 
127
122
  os.mkdir(automations_folder_path)
128
123
 
@@ -130,15 +125,11 @@ class BackupManager(BaseManager):
130
125
  if len(automations["data"]) > 0:
131
126
  self.json_utils.dump(automations_file_path, automations)
132
127
 
133
- def store_declarative_filter_views(
134
- self, export_path: Path, workspace_id: str
135
- ) -> None:
128
+ def store_declarative_filter_views(self, export_path: Path, workspace_id: str) -> None:
136
129
  """Stores the filter views in the specified export path."""
137
130
  # Get the filter views YAML files from the API
138
131
  with self._api_rate_limiter:
139
- self._api._sdk.catalog_workspace.store_declarative_filter_views(
140
- workspace_id, export_path
141
- )
132
+ self._api._sdk.catalog_workspace.store_declarative_filter_views(workspace_id, export_path)
142
133
 
143
134
  # Move filter views to the subfolder containing the analytics model
144
135
  self._move_folder(
@@ -176,8 +167,7 @@ class BackupManager(BaseManager):
176
167
  user_data_filters = self.get_user_data_filters(workspace_id)
177
168
  except Exception as e:
178
169
  self.logger.error(
179
- f"Skipping backup of {workspace_id} - check if workspace exists."
180
- + f"{e.__class__.__name__}: {e}"
170
+ f"Skipping backup of {workspace_id} - check if workspace exists." + f"{e.__class__.__name__}: {e}"
181
171
  )
182
172
  continue
183
173
 
@@ -188,33 +178,23 @@ class BackupManager(BaseManager):
188
178
  # directly instead of reorganizing the folder structures. That should
189
179
  # be more transparent/readable and possibly safer for threading
190
180
  with self._api_rate_limiter:
191
- self._api._sdk.catalog_workspace.store_declarative_workspace(
192
- workspace_id, export_path
193
- )
181
+ self._api._sdk.catalog_workspace.store_declarative_workspace(workspace_id, export_path)
194
182
  self.store_declarative_filter_views(export_path, workspace_id)
195
183
  self._store_automations(export_path, workspace_id)
196
184
 
197
- self._store_user_data_filters(
198
- user_data_filters, export_path, workspace_id
199
- )
185
+ self._store_user_data_filters(user_data_filters, export_path, workspace_id)
200
186
 
201
- workspace_duration_ms = int(
202
- (time.time() - workspace_start_time) * 1000
203
- )
187
+ workspace_duration_ms = int((time.time() - workspace_start_time) * 1000)
204
188
  zip_file_path = f"{self.org_id}/{workspace_id}.zip"
205
189
 
206
190
  self.logger.info(
207
- f"Completed backup for workspace {workspace_id} "
208
- f"to {zip_file_path} in {workspace_duration_ms}ms"
191
+ f"Completed backup for workspace {workspace_id} to {zip_file_path} in {workspace_duration_ms}ms"
209
192
  )
210
193
  exported = True
211
194
  except Exception as e:
212
- workspace_duration_ms = int(
213
- (time.time() - workspace_start_time) * 1000
214
- )
195
+ workspace_duration_ms = int((time.time() - workspace_start_time) * 1000)
215
196
  self.logger.error(
216
- f"Skipping {workspace_id} after {workspace_duration_ms}ms. "
217
- f"{e.__class__.__name__} encountered: {e}"
197
+ f"Skipping {workspace_id} after {workspace_duration_ms}ms. {e.__class__.__name__} encountered: {e}"
218
198
  )
219
199
 
220
200
  if not exported:
@@ -245,9 +225,7 @@ class BackupManager(BaseManager):
245
225
  raise
246
226
 
247
227
  @staticmethod
248
- def _split_to_batches(
249
- workspaces_to_export: list[str], batch_size: int
250
- ) -> list[BackupBatch]:
228
+ def _split_to_batches(workspaces_to_export: list[str], batch_size: int) -> list[BackupBatch]:
251
229
  """Splits the list of workspaces into batches of the specified size.
252
230
  The batch is represented as a list of workspace IDs.
253
231
  Returns a list of batches (i.e. list of lists of IDs)
@@ -274,9 +252,7 @@ class BackupManager(BaseManager):
274
252
  with tempfile.TemporaryDirectory() as tmpdir:
275
253
  self._get_workspace_export(tmpdir, batch.list_of_ids)
276
254
 
277
- self._archive_gooddata_layouts_to_zip(
278
- str(Path(tmpdir, self.org_id))
279
- )
255
+ self._archive_gooddata_layouts_to_zip(str(Path(tmpdir, self.org_id)))
280
256
 
281
257
  self.storage.export(tmpdir, self.org_id)
282
258
 
@@ -367,13 +343,9 @@ class BackupManager(BaseManager):
367
343
  path_to_csv,
368
344
  workspace_ids,
369
345
  )
370
- batches = self._split_to_batches(
371
- workspaces_to_export, self.config.batch_size
372
- )
346
+ batches = self._split_to_batches(workspaces_to_export, self.config.batch_size)
373
347
 
374
- self.logger.info(
375
- f"Exporting {len(workspaces_to_export)} workspaces in {len(batches)} batches."
376
- )
348
+ self.logger.info(f"Exporting {len(workspaces_to_export)} workspaces in {len(batches)} batches.")
377
349
 
378
350
  self._process_batches(batches)
379
351
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import abc
4
4
  from pathlib import Path
5
- from typing import Type, TypeVar
5
+ from typing import TypeVar
6
6
 
7
7
  from gooddata_sdk.utils import PROFILES_FILE_PATH, profile_content
8
8
 
@@ -11,6 +11,9 @@ from gooddata_pipelines.backup_and_restore.models.storage import (
11
11
  BackupRestoreConfig,
12
12
  StorageType,
13
13
  )
14
+ from gooddata_pipelines.backup_and_restore.storage.azure_storage import (
15
+ AzureStorage,
16
+ )
14
17
  from gooddata_pipelines.backup_and_restore.storage.base_storage import (
15
18
  BackupStorage,
16
19
  )
@@ -18,9 +21,6 @@ from gooddata_pipelines.backup_and_restore.storage.local_storage import (
18
21
  LocalStorage,
19
22
  )
20
23
  from gooddata_pipelines.backup_and_restore.storage.s3_storage import S3Storage
21
- from gooddata_pipelines.backup_and_restore.storage.azure_storage import (
22
- AzureStorage,
23
- )
24
24
  from gooddata_pipelines.logger import LogObserver
25
25
  from gooddata_pipelines.utils.file_utils import JsonUtils, YamlUtils
26
26
 
@@ -52,13 +52,11 @@ class BaseManager(abc.ABC):
52
52
  elif conf.storage_type == StorageType.LOCAL:
53
53
  return LocalStorage(conf)
54
54
  else:
55
- raise RuntimeError(
56
- f'Unsupported storage type "{conf.storage_type.value}".'
57
- )
55
+ raise RuntimeError(f'Unsupported storage type "{conf.storage_type.value}".')
58
56
 
59
57
  @classmethod
60
58
  def create(
61
- cls: Type[ManagerT],
59
+ cls: type[ManagerT],
62
60
  config: BackupRestoreConfig,
63
61
  host: str,
64
62
  token: str,
@@ -68,7 +66,7 @@ class BaseManager(abc.ABC):
68
66
 
69
67
  @classmethod
70
68
  def create_from_profile(
71
- cls: Type[ManagerT],
69
+ cls: type[ManagerT],
72
70
  config: BackupRestoreConfig,
73
71
  profile: str = "default",
74
72
  profiles_path: Path = PROFILES_FILE_PATH,
@@ -33,8 +33,4 @@ class BackupSettings:
33
33
  API = ApiDefaults()
34
34
  MAX_RETRIES = 3
35
35
  RETRY_DELAY = 5 # seconds
36
- TIMESTAMP_SDK_FOLDER = (
37
- str(datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
38
- + "-"
39
- + sdk_version.replace(".", "_")
40
- )
36
+ TIMESTAMP_SDK_FOLDER = str(datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) + "-" + sdk_version.replace(".", "_")
@@ -14,18 +14,14 @@ class CSVReader:
14
14
  """
15
15
 
16
16
  with open(file_path) as csv_file:
17
- reader: Iterator[list[str]] = csv.reader(
18
- csv_file, skipinitialspace=True
19
- )
17
+ reader: Iterator[list[str]] = csv.reader(csv_file, skipinitialspace=True)
20
18
 
21
19
  try:
22
20
  # Skip the header
23
21
  headers = next(reader)
24
22
 
25
23
  if len(headers) > 1:
26
- raise ValueError(
27
- "Input file contains more than one column. Please check the input and try again."
28
- )
24
+ raise ValueError("Input file contains more than one column. Please check the input and try again.")
29
25
 
30
26
  except StopIteration:
31
27
  # Raise an error if the iterator is empty