workato-platform-cli 1.0.0rc5.dev5__py3-none-any.whl

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 (306) hide show
  1. workato_platform_cli/__init__.py +135 -0
  2. workato_platform_cli/_version.py +34 -0
  3. workato_platform_cli/cli/__init__.py +126 -0
  4. workato_platform_cli/cli/commands/__init__.py +0 -0
  5. workato_platform_cli/cli/commands/api_clients.py +627 -0
  6. workato_platform_cli/cli/commands/api_collections.py +497 -0
  7. workato_platform_cli/cli/commands/assets.py +82 -0
  8. workato_platform_cli/cli/commands/connections.py +1205 -0
  9. workato_platform_cli/cli/commands/connectors/__init__.py +0 -0
  10. workato_platform_cli/cli/commands/connectors/command.py +178 -0
  11. workato_platform_cli/cli/commands/connectors/connector_manager.py +351 -0
  12. workato_platform_cli/cli/commands/data_tables.py +345 -0
  13. workato_platform_cli/cli/commands/guide.py +315 -0
  14. workato_platform_cli/cli/commands/init.py +229 -0
  15. workato_platform_cli/cli/commands/profiles.py +364 -0
  16. workato_platform_cli/cli/commands/projects/__init__.py +0 -0
  17. workato_platform_cli/cli/commands/projects/command.py +513 -0
  18. workato_platform_cli/cli/commands/projects/project_manager.py +338 -0
  19. workato_platform_cli/cli/commands/properties.py +174 -0
  20. workato_platform_cli/cli/commands/pull.py +327 -0
  21. workato_platform_cli/cli/commands/push/__init__.py +0 -0
  22. workato_platform_cli/cli/commands/push/command.py +320 -0
  23. workato_platform_cli/cli/commands/recipes/__init__.py +0 -0
  24. workato_platform_cli/cli/commands/recipes/command.py +847 -0
  25. workato_platform_cli/cli/commands/recipes/validator.py +1740 -0
  26. workato_platform_cli/cli/commands/workspace.py +73 -0
  27. workato_platform_cli/cli/containers.py +80 -0
  28. workato_platform_cli/cli/resources/data/connection-data.json +7364 -0
  29. workato_platform_cli/cli/resources/data/picklist-data.json +3706 -0
  30. workato_platform_cli/cli/resources/docs/README.md +178 -0
  31. workato_platform_cli/cli/resources/docs/actions.md +452 -0
  32. workato_platform_cli/cli/resources/docs/block-structure.md +424 -0
  33. workato_platform_cli/cli/resources/docs/connections-parameters.md +11946 -0
  34. workato_platform_cli/cli/resources/docs/data-mapping.md +779 -0
  35. workato_platform_cli/cli/resources/docs/formulas/array-list-formulas.md +1276 -0
  36. workato_platform_cli/cli/resources/docs/formulas/conditions.md +102 -0
  37. workato_platform_cli/cli/resources/docs/formulas/date-formulas.md +798 -0
  38. workato_platform_cli/cli/resources/docs/formulas/number-formulas.md +507 -0
  39. workato_platform_cli/cli/resources/docs/formulas/other-formulas.md +419 -0
  40. workato_platform_cli/cli/resources/docs/formulas/string-formulas.md +1353 -0
  41. workato_platform_cli/cli/resources/docs/formulas.md +214 -0
  42. workato_platform_cli/cli/resources/docs/naming-conventions.md +163 -0
  43. workato_platform_cli/cli/resources/docs/recipe-deployment-workflow.md +352 -0
  44. workato_platform_cli/cli/resources/docs/recipe-fundamentals.md +179 -0
  45. workato_platform_cli/cli/resources/docs/triggers.md +360 -0
  46. workato_platform_cli/cli/utils/__init__.py +10 -0
  47. workato_platform_cli/cli/utils/config/__init__.py +33 -0
  48. workato_platform_cli/cli/utils/config/manager.py +1001 -0
  49. workato_platform_cli/cli/utils/config/models.py +89 -0
  50. workato_platform_cli/cli/utils/config/profiles.py +491 -0
  51. workato_platform_cli/cli/utils/config/workspace.py +113 -0
  52. workato_platform_cli/cli/utils/exception_handler.py +531 -0
  53. workato_platform_cli/cli/utils/gitignore.py +32 -0
  54. workato_platform_cli/cli/utils/ignore_patterns.py +44 -0
  55. workato_platform_cli/cli/utils/spinner.py +63 -0
  56. workato_platform_cli/cli/utils/version_checker.py +237 -0
  57. workato_platform_cli/client/__init__.py +0 -0
  58. workato_platform_cli/client/workato_api/__init__.py +202 -0
  59. workato_platform_cli/client/workato_api/api/__init__.py +15 -0
  60. workato_platform_cli/client/workato_api/api/api_platform_api.py +2875 -0
  61. workato_platform_cli/client/workato_api/api/connections_api.py +1807 -0
  62. workato_platform_cli/client/workato_api/api/connectors_api.py +840 -0
  63. workato_platform_cli/client/workato_api/api/data_tables_api.py +604 -0
  64. workato_platform_cli/client/workato_api/api/export_api.py +621 -0
  65. workato_platform_cli/client/workato_api/api/folders_api.py +621 -0
  66. workato_platform_cli/client/workato_api/api/packages_api.py +1197 -0
  67. workato_platform_cli/client/workato_api/api/projects_api.py +590 -0
  68. workato_platform_cli/client/workato_api/api/properties_api.py +620 -0
  69. workato_platform_cli/client/workato_api/api/recipes_api.py +1379 -0
  70. workato_platform_cli/client/workato_api/api/users_api.py +285 -0
  71. workato_platform_cli/client/workato_api/api_client.py +807 -0
  72. workato_platform_cli/client/workato_api/api_response.py +21 -0
  73. workato_platform_cli/client/workato_api/configuration.py +601 -0
  74. workato_platform_cli/client/workato_api/docs/APIPlatformApi.md +844 -0
  75. workato_platform_cli/client/workato_api/docs/ApiClient.md +46 -0
  76. workato_platform_cli/client/workato_api/docs/ApiClientApiCollectionsInner.md +30 -0
  77. workato_platform_cli/client/workato_api/docs/ApiClientApiPoliciesInner.md +30 -0
  78. workato_platform_cli/client/workato_api/docs/ApiClientCreateRequest.md +46 -0
  79. workato_platform_cli/client/workato_api/docs/ApiClientListResponse.md +32 -0
  80. workato_platform_cli/client/workato_api/docs/ApiClientResponse.md +29 -0
  81. workato_platform_cli/client/workato_api/docs/ApiCollection.md +38 -0
  82. workato_platform_cli/client/workato_api/docs/ApiCollectionCreateRequest.md +32 -0
  83. workato_platform_cli/client/workato_api/docs/ApiEndpoint.md +41 -0
  84. workato_platform_cli/client/workato_api/docs/ApiKey.md +36 -0
  85. workato_platform_cli/client/workato_api/docs/ApiKeyCreateRequest.md +32 -0
  86. workato_platform_cli/client/workato_api/docs/ApiKeyListResponse.md +32 -0
  87. workato_platform_cli/client/workato_api/docs/ApiKeyResponse.md +29 -0
  88. workato_platform_cli/client/workato_api/docs/Asset.md +39 -0
  89. workato_platform_cli/client/workato_api/docs/AssetReference.md +37 -0
  90. workato_platform_cli/client/workato_api/docs/Connection.md +44 -0
  91. workato_platform_cli/client/workato_api/docs/ConnectionCreateRequest.md +35 -0
  92. workato_platform_cli/client/workato_api/docs/ConnectionUpdateRequest.md +34 -0
  93. workato_platform_cli/client/workato_api/docs/ConnectionsApi.md +526 -0
  94. workato_platform_cli/client/workato_api/docs/ConnectorAction.md +33 -0
  95. workato_platform_cli/client/workato_api/docs/ConnectorVersion.md +32 -0
  96. workato_platform_cli/client/workato_api/docs/ConnectorsApi.md +249 -0
  97. workato_platform_cli/client/workato_api/docs/CreateExportManifestRequest.md +29 -0
  98. workato_platform_cli/client/workato_api/docs/CreateFolderRequest.md +30 -0
  99. workato_platform_cli/client/workato_api/docs/CustomConnector.md +35 -0
  100. workato_platform_cli/client/workato_api/docs/CustomConnectorCodeResponse.md +29 -0
  101. workato_platform_cli/client/workato_api/docs/CustomConnectorCodeResponseData.md +29 -0
  102. workato_platform_cli/client/workato_api/docs/CustomConnectorListResponse.md +29 -0
  103. workato_platform_cli/client/workato_api/docs/DataTable.md +34 -0
  104. workato_platform_cli/client/workato_api/docs/DataTableColumn.md +37 -0
  105. workato_platform_cli/client/workato_api/docs/DataTableColumnRequest.md +37 -0
  106. workato_platform_cli/client/workato_api/docs/DataTableCreateRequest.md +31 -0
  107. workato_platform_cli/client/workato_api/docs/DataTableCreateResponse.md +29 -0
  108. workato_platform_cli/client/workato_api/docs/DataTableListResponse.md +29 -0
  109. workato_platform_cli/client/workato_api/docs/DataTableRelation.md +30 -0
  110. workato_platform_cli/client/workato_api/docs/DataTablesApi.md +172 -0
  111. workato_platform_cli/client/workato_api/docs/DeleteProject403Response.md +29 -0
  112. workato_platform_cli/client/workato_api/docs/Error.md +29 -0
  113. workato_platform_cli/client/workato_api/docs/ExportApi.md +175 -0
  114. workato_platform_cli/client/workato_api/docs/ExportManifestRequest.md +35 -0
  115. workato_platform_cli/client/workato_api/docs/ExportManifestResponse.md +29 -0
  116. workato_platform_cli/client/workato_api/docs/ExportManifestResponseResult.md +36 -0
  117. workato_platform_cli/client/workato_api/docs/Folder.md +35 -0
  118. workato_platform_cli/client/workato_api/docs/FolderAssetsResponse.md +29 -0
  119. workato_platform_cli/client/workato_api/docs/FolderAssetsResponseResult.md +29 -0
  120. workato_platform_cli/client/workato_api/docs/FolderCreationResponse.md +35 -0
  121. workato_platform_cli/client/workato_api/docs/FoldersApi.md +176 -0
  122. workato_platform_cli/client/workato_api/docs/ImportResults.md +32 -0
  123. workato_platform_cli/client/workato_api/docs/OAuthUrlResponse.md +29 -0
  124. workato_platform_cli/client/workato_api/docs/OAuthUrlResponseData.md +29 -0
  125. workato_platform_cli/client/workato_api/docs/OpenApiSpec.md +30 -0
  126. workato_platform_cli/client/workato_api/docs/PackageDetailsResponse.md +35 -0
  127. workato_platform_cli/client/workato_api/docs/PackageDetailsResponseRecipeStatusInner.md +30 -0
  128. workato_platform_cli/client/workato_api/docs/PackageResponse.md +33 -0
  129. workato_platform_cli/client/workato_api/docs/PackagesApi.md +364 -0
  130. workato_platform_cli/client/workato_api/docs/PicklistRequest.md +30 -0
  131. workato_platform_cli/client/workato_api/docs/PicklistResponse.md +29 -0
  132. workato_platform_cli/client/workato_api/docs/PlatformConnector.md +36 -0
  133. workato_platform_cli/client/workato_api/docs/PlatformConnectorListResponse.md +32 -0
  134. workato_platform_cli/client/workato_api/docs/Project.md +32 -0
  135. workato_platform_cli/client/workato_api/docs/ProjectsApi.md +173 -0
  136. workato_platform_cli/client/workato_api/docs/PropertiesApi.md +186 -0
  137. workato_platform_cli/client/workato_api/docs/Recipe.md +58 -0
  138. workato_platform_cli/client/workato_api/docs/RecipeConfigInner.md +33 -0
  139. workato_platform_cli/client/workato_api/docs/RecipeConnectionUpdateRequest.md +30 -0
  140. workato_platform_cli/client/workato_api/docs/RecipeListResponse.md +29 -0
  141. workato_platform_cli/client/workato_api/docs/RecipeStartResponse.md +31 -0
  142. workato_platform_cli/client/workato_api/docs/RecipesApi.md +367 -0
  143. workato_platform_cli/client/workato_api/docs/RuntimeUserConnectionCreateRequest.md +34 -0
  144. workato_platform_cli/client/workato_api/docs/RuntimeUserConnectionResponse.md +29 -0
  145. workato_platform_cli/client/workato_api/docs/RuntimeUserConnectionResponseData.md +30 -0
  146. workato_platform_cli/client/workato_api/docs/SuccessResponse.md +29 -0
  147. workato_platform_cli/client/workato_api/docs/UpsertProjectPropertiesRequest.md +29 -0
  148. workato_platform_cli/client/workato_api/docs/User.md +48 -0
  149. workato_platform_cli/client/workato_api/docs/UsersApi.md +84 -0
  150. workato_platform_cli/client/workato_api/docs/ValidationError.md +30 -0
  151. workato_platform_cli/client/workato_api/docs/ValidationErrorErrorsValue.md +28 -0
  152. workato_platform_cli/client/workato_api/exceptions.py +216 -0
  153. workato_platform_cli/client/workato_api/models/__init__.py +83 -0
  154. workato_platform_cli/client/workato_api/models/api_client.py +185 -0
  155. workato_platform_cli/client/workato_api/models/api_client_api_collections_inner.py +89 -0
  156. workato_platform_cli/client/workato_api/models/api_client_api_policies_inner.py +89 -0
  157. workato_platform_cli/client/workato_api/models/api_client_create_request.py +138 -0
  158. workato_platform_cli/client/workato_api/models/api_client_list_response.py +101 -0
  159. workato_platform_cli/client/workato_api/models/api_client_response.py +91 -0
  160. workato_platform_cli/client/workato_api/models/api_collection.py +110 -0
  161. workato_platform_cli/client/workato_api/models/api_collection_create_request.py +97 -0
  162. workato_platform_cli/client/workato_api/models/api_endpoint.py +117 -0
  163. workato_platform_cli/client/workato_api/models/api_key.py +102 -0
  164. workato_platform_cli/client/workato_api/models/api_key_create_request.py +93 -0
  165. workato_platform_cli/client/workato_api/models/api_key_list_response.py +101 -0
  166. workato_platform_cli/client/workato_api/models/api_key_response.py +91 -0
  167. workato_platform_cli/client/workato_api/models/asset.py +124 -0
  168. workato_platform_cli/client/workato_api/models/asset_reference.py +110 -0
  169. workato_platform_cli/client/workato_api/models/connection.py +173 -0
  170. workato_platform_cli/client/workato_api/models/connection_create_request.py +99 -0
  171. workato_platform_cli/client/workato_api/models/connection_update_request.py +97 -0
  172. workato_platform_cli/client/workato_api/models/connector_action.py +100 -0
  173. workato_platform_cli/client/workato_api/models/connector_version.py +99 -0
  174. workato_platform_cli/client/workato_api/models/create_export_manifest_request.py +91 -0
  175. workato_platform_cli/client/workato_api/models/create_folder_request.py +89 -0
  176. workato_platform_cli/client/workato_api/models/custom_connector.py +117 -0
  177. workato_platform_cli/client/workato_api/models/custom_connector_code_response.py +91 -0
  178. workato_platform_cli/client/workato_api/models/custom_connector_code_response_data.py +87 -0
  179. workato_platform_cli/client/workato_api/models/custom_connector_list_response.py +95 -0
  180. workato_platform_cli/client/workato_api/models/data_table.py +107 -0
  181. workato_platform_cli/client/workato_api/models/data_table_column.py +125 -0
  182. workato_platform_cli/client/workato_api/models/data_table_column_request.py +130 -0
  183. workato_platform_cli/client/workato_api/models/data_table_create_request.py +99 -0
  184. workato_platform_cli/client/workato_api/models/data_table_create_response.py +91 -0
  185. workato_platform_cli/client/workato_api/models/data_table_list_response.py +95 -0
  186. workato_platform_cli/client/workato_api/models/data_table_relation.py +90 -0
  187. workato_platform_cli/client/workato_api/models/delete_project403_response.py +87 -0
  188. workato_platform_cli/client/workato_api/models/error.py +87 -0
  189. workato_platform_cli/client/workato_api/models/export_manifest_request.py +107 -0
  190. workato_platform_cli/client/workato_api/models/export_manifest_response.py +91 -0
  191. workato_platform_cli/client/workato_api/models/export_manifest_response_result.py +112 -0
  192. workato_platform_cli/client/workato_api/models/folder.py +110 -0
  193. workato_platform_cli/client/workato_api/models/folder_assets_response.py +91 -0
  194. workato_platform_cli/client/workato_api/models/folder_assets_response_result.py +95 -0
  195. workato_platform_cli/client/workato_api/models/folder_creation_response.py +110 -0
  196. workato_platform_cli/client/workato_api/models/import_results.py +93 -0
  197. workato_platform_cli/client/workato_api/models/o_auth_url_response.py +91 -0
  198. workato_platform_cli/client/workato_api/models/o_auth_url_response_data.py +87 -0
  199. workato_platform_cli/client/workato_api/models/open_api_spec.py +96 -0
  200. workato_platform_cli/client/workato_api/models/package_details_response.py +126 -0
  201. workato_platform_cli/client/workato_api/models/package_details_response_recipe_status_inner.py +99 -0
  202. workato_platform_cli/client/workato_api/models/package_response.py +109 -0
  203. workato_platform_cli/client/workato_api/models/picklist_request.py +89 -0
  204. workato_platform_cli/client/workato_api/models/picklist_response.py +88 -0
  205. workato_platform_cli/client/workato_api/models/platform_connector.py +116 -0
  206. workato_platform_cli/client/workato_api/models/platform_connector_list_response.py +101 -0
  207. workato_platform_cli/client/workato_api/models/project.py +93 -0
  208. workato_platform_cli/client/workato_api/models/recipe.py +174 -0
  209. workato_platform_cli/client/workato_api/models/recipe_config_inner.py +100 -0
  210. workato_platform_cli/client/workato_api/models/recipe_connection_update_request.py +89 -0
  211. workato_platform_cli/client/workato_api/models/recipe_list_response.py +95 -0
  212. workato_platform_cli/client/workato_api/models/recipe_start_response.py +91 -0
  213. workato_platform_cli/client/workato_api/models/runtime_user_connection_create_request.py +97 -0
  214. workato_platform_cli/client/workato_api/models/runtime_user_connection_response.py +91 -0
  215. workato_platform_cli/client/workato_api/models/runtime_user_connection_response_data.py +89 -0
  216. workato_platform_cli/client/workato_api/models/success_response.py +87 -0
  217. workato_platform_cli/client/workato_api/models/upsert_project_properties_request.py +88 -0
  218. workato_platform_cli/client/workato_api/models/user.py +151 -0
  219. workato_platform_cli/client/workato_api/models/validation_error.py +102 -0
  220. workato_platform_cli/client/workato_api/models/validation_error_errors_value.py +143 -0
  221. workato_platform_cli/client/workato_api/rest.py +213 -0
  222. workato_platform_cli/client/workato_api/test/__init__.py +0 -0
  223. workato_platform_cli/client/workato_api/test/test_api_client.py +94 -0
  224. workato_platform_cli/client/workato_api/test/test_api_client_api_collections_inner.py +52 -0
  225. workato_platform_cli/client/workato_api/test/test_api_client_api_policies_inner.py +52 -0
  226. workato_platform_cli/client/workato_api/test/test_api_client_create_request.py +75 -0
  227. workato_platform_cli/client/workato_api/test/test_api_client_list_response.py +114 -0
  228. workato_platform_cli/client/workato_api/test/test_api_client_response.py +104 -0
  229. workato_platform_cli/client/workato_api/test/test_api_collection.py +72 -0
  230. workato_platform_cli/client/workato_api/test/test_api_collection_create_request.py +57 -0
  231. workato_platform_cli/client/workato_api/test/test_api_endpoint.py +75 -0
  232. workato_platform_cli/client/workato_api/test/test_api_key.py +64 -0
  233. workato_platform_cli/client/workato_api/test/test_api_key_create_request.py +56 -0
  234. workato_platform_cli/client/workato_api/test/test_api_key_list_response.py +78 -0
  235. workato_platform_cli/client/workato_api/test/test_api_key_response.py +68 -0
  236. workato_platform_cli/client/workato_api/test/test_api_platform_api.py +101 -0
  237. workato_platform_cli/client/workato_api/test/test_asset.py +67 -0
  238. workato_platform_cli/client/workato_api/test/test_asset_reference.py +62 -0
  239. workato_platform_cli/client/workato_api/test/test_connection.py +81 -0
  240. workato_platform_cli/client/workato_api/test/test_connection_create_request.py +59 -0
  241. workato_platform_cli/client/workato_api/test/test_connection_update_request.py +56 -0
  242. workato_platform_cli/client/workato_api/test/test_connections_api.py +73 -0
  243. workato_platform_cli/client/workato_api/test/test_connector_action.py +59 -0
  244. workato_platform_cli/client/workato_api/test/test_connector_version.py +58 -0
  245. workato_platform_cli/client/workato_api/test/test_connectors_api.py +52 -0
  246. workato_platform_cli/client/workato_api/test/test_create_export_manifest_request.py +88 -0
  247. workato_platform_cli/client/workato_api/test/test_create_folder_request.py +53 -0
  248. workato_platform_cli/client/workato_api/test/test_custom_connector.py +76 -0
  249. workato_platform_cli/client/workato_api/test/test_custom_connector_code_response.py +54 -0
  250. workato_platform_cli/client/workato_api/test/test_custom_connector_code_response_data.py +52 -0
  251. workato_platform_cli/client/workato_api/test/test_custom_connector_list_response.py +82 -0
  252. workato_platform_cli/client/workato_api/test/test_data_table.py +88 -0
  253. workato_platform_cli/client/workato_api/test/test_data_table_column.py +72 -0
  254. workato_platform_cli/client/workato_api/test/test_data_table_column_request.py +64 -0
  255. workato_platform_cli/client/workato_api/test/test_data_table_create_request.py +82 -0
  256. workato_platform_cli/client/workato_api/test/test_data_table_create_response.py +90 -0
  257. workato_platform_cli/client/workato_api/test/test_data_table_list_response.py +94 -0
  258. workato_platform_cli/client/workato_api/test/test_data_table_relation.py +54 -0
  259. workato_platform_cli/client/workato_api/test/test_data_tables_api.py +45 -0
  260. workato_platform_cli/client/workato_api/test/test_delete_project403_response.py +51 -0
  261. workato_platform_cli/client/workato_api/test/test_error.py +52 -0
  262. workato_platform_cli/client/workato_api/test/test_export_api.py +45 -0
  263. workato_platform_cli/client/workato_api/test/test_export_manifest_request.py +69 -0
  264. workato_platform_cli/client/workato_api/test/test_export_manifest_response.py +68 -0
  265. workato_platform_cli/client/workato_api/test/test_export_manifest_response_result.py +66 -0
  266. workato_platform_cli/client/workato_api/test/test_folder.py +64 -0
  267. workato_platform_cli/client/workato_api/test/test_folder_assets_response.py +80 -0
  268. workato_platform_cli/client/workato_api/test/test_folder_assets_response_result.py +78 -0
  269. workato_platform_cli/client/workato_api/test/test_folder_creation_response.py +64 -0
  270. workato_platform_cli/client/workato_api/test/test_folders_api.py +45 -0
  271. workato_platform_cli/client/workato_api/test/test_import_results.py +58 -0
  272. workato_platform_cli/client/workato_api/test/test_o_auth_url_response.py +54 -0
  273. workato_platform_cli/client/workato_api/test/test_o_auth_url_response_data.py +52 -0
  274. workato_platform_cli/client/workato_api/test/test_open_api_spec.py +54 -0
  275. workato_platform_cli/client/workato_api/test/test_package_details_response.py +64 -0
  276. workato_platform_cli/client/workato_api/test/test_package_details_response_recipe_status_inner.py +52 -0
  277. workato_platform_cli/client/workato_api/test/test_package_response.py +58 -0
  278. workato_platform_cli/client/workato_api/test/test_packages_api.py +59 -0
  279. workato_platform_cli/client/workato_api/test/test_picklist_request.py +53 -0
  280. workato_platform_cli/client/workato_api/test/test_picklist_response.py +52 -0
  281. workato_platform_cli/client/workato_api/test/test_platform_connector.py +94 -0
  282. workato_platform_cli/client/workato_api/test/test_platform_connector_list_response.py +106 -0
  283. workato_platform_cli/client/workato_api/test/test_project.py +57 -0
  284. workato_platform_cli/client/workato_api/test/test_projects_api.py +45 -0
  285. workato_platform_cli/client/workato_api/test/test_properties_api.py +45 -0
  286. workato_platform_cli/client/workato_api/test/test_recipe.py +124 -0
  287. workato_platform_cli/client/workato_api/test/test_recipe_config_inner.py +55 -0
  288. workato_platform_cli/client/workato_api/test/test_recipe_connection_update_request.py +54 -0
  289. workato_platform_cli/client/workato_api/test/test_recipe_list_response.py +134 -0
  290. workato_platform_cli/client/workato_api/test/test_recipe_start_response.py +54 -0
  291. workato_platform_cli/client/workato_api/test/test_recipes_api.py +59 -0
  292. workato_platform_cli/client/workato_api/test/test_runtime_user_connection_create_request.py +59 -0
  293. workato_platform_cli/client/workato_api/test/test_runtime_user_connection_response.py +56 -0
  294. workato_platform_cli/client/workato_api/test/test_runtime_user_connection_response_data.py +54 -0
  295. workato_platform_cli/client/workato_api/test/test_success_response.py +52 -0
  296. workato_platform_cli/client/workato_api/test/test_upsert_project_properties_request.py +52 -0
  297. workato_platform_cli/client/workato_api/test/test_user.py +85 -0
  298. workato_platform_cli/client/workato_api/test/test_users_api.py +38 -0
  299. workato_platform_cli/client/workato_api/test/test_validation_error.py +52 -0
  300. workato_platform_cli/client/workato_api/test/test_validation_error_errors_value.py +50 -0
  301. workato_platform_cli/client/workato_api_README.md +205 -0
  302. workato_platform_cli-1.0.0rc5.dev5.dist-info/METADATA +185 -0
  303. workato_platform_cli-1.0.0rc5.dev5.dist-info/RECORD +306 -0
  304. workato_platform_cli-1.0.0rc5.dev5.dist-info/WHEEL +4 -0
  305. workato_platform_cli-1.0.0rc5.dev5.dist-info/entry_points.txt +2 -0
  306. workato_platform_cli-1.0.0rc5.dev5.dist-info/licenses/LICENSE +7 -0
@@ -0,0 +1,89 @@
1
+ """Data models for configuration management."""
2
+
3
+ from pydantic import BaseModel, Field, field_validator
4
+
5
+
6
+ class ProjectInfo(BaseModel):
7
+ """Data model for project information"""
8
+
9
+ id: int = Field(..., description="Project ID")
10
+ name: str = Field(..., description="Project name")
11
+ folder_id: int | None = Field(None, description="Associated folder ID")
12
+
13
+
14
+ class ConfigData(BaseModel):
15
+ """Data model for configuration file data"""
16
+
17
+ project_id: int | None = Field(None, description="Project ID")
18
+ project_name: str | None = Field(None, description="Project name")
19
+ project_path: str | None = Field(
20
+ None, description="Relative path to project (workspace only)"
21
+ )
22
+ folder_id: int | None = Field(None, description="Folder ID")
23
+ profile: str | None = Field(None, description="Profile override")
24
+
25
+
26
+ class RegionInfo(BaseModel):
27
+ """Data model for region information"""
28
+
29
+ region: str = Field(..., description="Region code")
30
+ name: str = Field(..., description="Human-readable region name")
31
+ url: str | None = Field(None, description="Base URL for the region")
32
+
33
+
34
+ class ProfileData(BaseModel):
35
+ """Data model for a single profile"""
36
+
37
+ region: str = Field(
38
+ ..., description="Region code (us, eu, jp, sg, au, il, trial, custom)"
39
+ )
40
+ region_url: str = Field(..., description="Base URL for the region")
41
+ workspace_id: int = Field(..., description="Workspace ID")
42
+
43
+ @field_validator("region")
44
+ def validate_region(cls, v: str) -> str: # noqa: N805
45
+ """Validate region code"""
46
+ valid_regions = {"us", "eu", "jp", "sg", "au", "il", "trial", "custom"}
47
+ if v not in valid_regions:
48
+ raise ValueError(f"Invalid region code: {v}")
49
+ return v
50
+
51
+ @property
52
+ def region_name(self) -> str:
53
+ """Get human-readable region name from region code"""
54
+ region_info = AVAILABLE_REGIONS.get(self.region)
55
+ return region_info.name if region_info else f"Unknown ({self.region})"
56
+
57
+
58
+ class ProfilesConfig(BaseModel):
59
+ """Data model for profiles file (~/.workato/profiles)"""
60
+
61
+ current_profile: str | None = Field(None, description="Currently active profile")
62
+ profiles: dict[str, ProfileData] = Field(
63
+ default_factory=dict, description="Profile definitions"
64
+ )
65
+
66
+
67
+ # Available Workato regions
68
+ AVAILABLE_REGIONS = {
69
+ "us": RegionInfo(region="us", name="US Data Center", url="https://www.workato.com"),
70
+ "eu": RegionInfo(
71
+ region="eu", name="EU Data Center", url="https://app.eu.workato.com"
72
+ ),
73
+ "jp": RegionInfo(
74
+ region="jp", name="JP Data Center", url="https://app.jp.workato.com"
75
+ ),
76
+ "sg": RegionInfo(
77
+ region="sg", name="SG Data Center", url="https://app.sg.workato.com"
78
+ ),
79
+ "au": RegionInfo(
80
+ region="au", name="AU Data Center", url="https://app.au.workato.com"
81
+ ),
82
+ "il": RegionInfo(
83
+ region="il", name="IL Data Center", url="https://app.il.workato.com"
84
+ ),
85
+ "trial": RegionInfo(
86
+ region="trial", name="Developer Sandbox", url="https://app.trial.workato.com"
87
+ ),
88
+ "custom": RegionInfo(region="custom", name="Custom URL", url=None),
89
+ }
@@ -0,0 +1,491 @@
1
+ """Profile management for multiple Workato environments."""
2
+
3
+ import contextlib
4
+ import json
5
+ import os
6
+ import threading
7
+
8
+ from pathlib import Path
9
+ from urllib.parse import urlparse
10
+
11
+ import asyncclick as click
12
+ import inquirer
13
+ import keyring
14
+
15
+ from keyring.backend import KeyringBackend
16
+ from keyring.compat import properties
17
+ from keyring.errors import KeyringError, NoKeyringError
18
+
19
+ from .models import AVAILABLE_REGIONS, ProfileData, ProfilesConfig, RegionInfo
20
+
21
+
22
+ def _validate_url_security(url: str) -> tuple[bool, str]:
23
+ """Validate URL security - only allow HTTP for localhost,
24
+ require HTTPS for others."""
25
+ if not url.startswith(("http://", "https://")):
26
+ return False, "URL must start with http:// or https://"
27
+
28
+ parsed = urlparse(url)
29
+
30
+ # Allow HTTP only for localhost/127.0.0.1
31
+ if parsed.scheme == "http":
32
+ hostname = parsed.hostname
33
+ if hostname not in ("localhost", "127.0.0.1", "::1"):
34
+ return (
35
+ False,
36
+ "HTTP URLs are only allowed for localhost. Use HTTPS for other hosts.",
37
+ )
38
+
39
+ return True, ""
40
+
41
+
42
+ def _set_secure_permissions(path: Path) -> None:
43
+ """Best-effort attempt to set secure file permissions."""
44
+ with contextlib.suppress(OSError):
45
+ path.chmod(0o600)
46
+
47
+
48
+ class _WorkatoFileKeyring(KeyringBackend):
49
+ """Fallback keyring that stores secrets in a local JSON file."""
50
+
51
+ @properties.classproperty
52
+ def priority(self) -> float:
53
+ return 0.1
54
+
55
+ def __init__(self, storage_path: Path) -> None:
56
+ super().__init__()
57
+ self._storage_path = storage_path
58
+ self._lock = threading.Lock()
59
+ self._ensure_storage_initialized()
60
+
61
+ def _ensure_storage_initialized(self) -> None:
62
+ self._storage_path.parent.mkdir(parents=True, exist_ok=True)
63
+ if not self._storage_path.exists():
64
+ self._storage_path.write_text("{}", encoding="utf-8")
65
+ _set_secure_permissions(self._storage_path)
66
+
67
+ def _load_data(self) -> dict[str, dict[str, str]]:
68
+ try:
69
+ raw = self._storage_path.read_text(encoding="utf-8")
70
+ except FileNotFoundError:
71
+ return {}
72
+ except OSError:
73
+ return {}
74
+
75
+ if not raw.strip():
76
+ return {}
77
+
78
+ try:
79
+ loaded = json.loads(raw)
80
+ except json.JSONDecodeError:
81
+ return {}
82
+
83
+ if isinstance(loaded, dict):
84
+ # Ensure nested dictionaries
85
+ normalized: dict[str, dict[str, str]] = {}
86
+ for service, usernames in loaded.items():
87
+ if isinstance(usernames, dict):
88
+ normalized[service] = {
89
+ str(username): str(password)
90
+ for username, password in usernames.items()
91
+ }
92
+ return normalized
93
+ return {}
94
+
95
+ def _save_data(self, data: dict[str, dict[str, str]]) -> None:
96
+ serialized = json.dumps(data, indent=2)
97
+ self._storage_path.write_text(serialized, encoding="utf-8")
98
+ _set_secure_permissions(self._storage_path)
99
+
100
+ def get_password(self, service: str, username: str) -> str | None:
101
+ with self._lock:
102
+ data = self._load_data()
103
+ return data.get(service, {}).get(username)
104
+
105
+ def set_password(self, service: str, username: str, password: str) -> None:
106
+ with self._lock:
107
+ data = self._load_data()
108
+ data.setdefault(service, {})[username] = password
109
+ self._save_data(data)
110
+
111
+ def delete_password(self, service: str, username: str) -> None:
112
+ with self._lock:
113
+ data = self._load_data()
114
+ usernames = data.get(service)
115
+ if usernames and username in usernames:
116
+ del usernames[username]
117
+ if not usernames:
118
+ del data[service]
119
+ self._save_data(data)
120
+
121
+
122
+ class ProfileManager:
123
+ """Manages profiles file configuration"""
124
+
125
+ def __init__(self) -> None:
126
+ """Initialize profile manager"""
127
+ self.global_config_dir = Path.home() / ".workato"
128
+ self.profiles_file = self.global_config_dir / "profiles"
129
+ self.keyring_service = "workato-platform-cli"
130
+ self._fallback_token_file = self.global_config_dir / "token_store.json"
131
+ self._using_fallback_keyring = False
132
+ self._ensure_keyring_backend()
133
+
134
+ def _ensure_keyring_backend(self, force_fallback: bool = False) -> None:
135
+ """Ensure a usable keyring backend is available for storing tokens."""
136
+ if os.environ.get("WORKATO_DISABLE_KEYRING", "").lower() == "true":
137
+ self._using_fallback_keyring = False
138
+ return
139
+
140
+ if force_fallback:
141
+ fallback_keyring = _WorkatoFileKeyring(self._fallback_token_file)
142
+ keyring.set_keyring(fallback_keyring)
143
+ self._using_fallback_keyring = True
144
+ return
145
+
146
+ try:
147
+ backend = keyring.get_keyring()
148
+ except Exception:
149
+ backend = None
150
+
151
+ backend_priority = getattr(backend, "priority", 0) if backend else 0
152
+ backend_module = getattr(backend, "__class__", type("", (), {})).__module__
153
+
154
+ if (
155
+ backend_priority
156
+ and backend_priority > 0
157
+ and not str(backend_module).startswith("keyring.backends.fail")
158
+ ):
159
+ # Perform a quick health check to ensure the backend is usable.
160
+ test_service = f"{self.keyring_service}-self-test"
161
+ test_username = "__workato__"
162
+ with contextlib.suppress(NoKeyringError, KeyringError, Exception):
163
+ backend = backend or keyring.get_keyring()
164
+ backend.set_password(test_service, test_username, "0")
165
+ backend.delete_password(test_service, test_username)
166
+ self._using_fallback_keyring = False
167
+ return
168
+
169
+ fallback_keyring = _WorkatoFileKeyring(self._fallback_token_file)
170
+ keyring.set_keyring(fallback_keyring)
171
+ self._using_fallback_keyring = True
172
+
173
+ def _is_keyring_enabled(self) -> bool:
174
+ """Check if keyring usage is enabled"""
175
+ return os.environ.get("WORKATO_DISABLE_KEYRING", "").lower() != "true"
176
+
177
+ def _get_token_from_keyring(self, profile_name: str) -> str | None:
178
+ """Get API token from keyring for the given profile"""
179
+ if not self._is_keyring_enabled():
180
+ return None
181
+
182
+ try:
183
+ pw: str | None = keyring.get_password(self.keyring_service, profile_name)
184
+ return pw
185
+ except NoKeyringError:
186
+ if not self._using_fallback_keyring:
187
+ self._ensure_keyring_backend(force_fallback=True)
188
+ if self._using_fallback_keyring:
189
+ with contextlib.suppress(NoKeyringError, KeyringError, Exception):
190
+ token: str | None = keyring.get_password(
191
+ self.keyring_service, profile_name
192
+ )
193
+ return token
194
+ return None
195
+ except KeyringError:
196
+ if not self._using_fallback_keyring:
197
+ self._ensure_keyring_backend(force_fallback=True)
198
+ if self._using_fallback_keyring:
199
+ with contextlib.suppress(NoKeyringError, KeyringError, Exception):
200
+ fallback_token: str | None = keyring.get_password(
201
+ self.keyring_service, profile_name
202
+ )
203
+ return fallback_token
204
+ return None
205
+ except Exception:
206
+ return None
207
+
208
+ def _store_token_in_keyring(self, profile_name: str, token: str) -> bool:
209
+ """Store API token in keyring for the given profile"""
210
+ if not self._is_keyring_enabled():
211
+ return False
212
+
213
+ try:
214
+ keyring.set_password(self.keyring_service, profile_name, token)
215
+ return True
216
+ except NoKeyringError:
217
+ if not self._using_fallback_keyring:
218
+ self._ensure_keyring_backend(force_fallback=True)
219
+ if self._using_fallback_keyring:
220
+ with contextlib.suppress(NoKeyringError, KeyringError, Exception):
221
+ keyring.set_password(self.keyring_service, profile_name, token)
222
+ return True
223
+ return False
224
+ except KeyringError:
225
+ if not self._using_fallback_keyring:
226
+ self._ensure_keyring_backend(force_fallback=True)
227
+ if self._using_fallback_keyring:
228
+ with contextlib.suppress(NoKeyringError, KeyringError, Exception):
229
+ keyring.set_password(self.keyring_service, profile_name, token)
230
+ return True
231
+ return False
232
+ except Exception:
233
+ return False
234
+
235
+ def _delete_token_from_keyring(self, profile_name: str) -> bool:
236
+ """Delete API token from keyring for the given profile"""
237
+ if not self._is_keyring_enabled():
238
+ return False
239
+
240
+ try:
241
+ keyring.delete_password(self.keyring_service, profile_name)
242
+ return True
243
+ except NoKeyringError:
244
+ if not self._using_fallback_keyring:
245
+ self._ensure_keyring_backend(force_fallback=True)
246
+ if self._using_fallback_keyring:
247
+ with contextlib.suppress(NoKeyringError, KeyringError, Exception):
248
+ keyring.delete_password(self.keyring_service, profile_name)
249
+ return True
250
+ return False
251
+ except KeyringError:
252
+ if not self._using_fallback_keyring:
253
+ self._ensure_keyring_backend(force_fallback=True)
254
+ if self._using_fallback_keyring:
255
+ with contextlib.suppress(NoKeyringError, KeyringError, Exception):
256
+ keyring.delete_password(self.keyring_service, profile_name)
257
+ return True
258
+ return False
259
+ except Exception:
260
+ return False
261
+
262
+ def _ensure_global_config_dir(self) -> None:
263
+ """Ensure global config directory exists with proper permissions"""
264
+ self.global_config_dir.mkdir(exist_ok=True, mode=0o700)
265
+
266
+ def load_profiles(self) -> ProfilesConfig:
267
+ """Load profiles configuration from file"""
268
+ if not self.profiles_file.exists():
269
+ return ProfilesConfig(current_profile=None, profiles={})
270
+
271
+ try:
272
+ with open(self.profiles_file) as f:
273
+ data = json.load(f)
274
+ if not isinstance(data, dict):
275
+ raise ValueError("Invalid profiles file")
276
+ config: ProfilesConfig = ProfilesConfig.model_validate(data)
277
+ return config
278
+ except (json.JSONDecodeError, ValueError):
279
+ return ProfilesConfig(current_profile=None, profiles={})
280
+
281
+ def save_profiles(self, profiles_config: ProfilesConfig) -> None:
282
+ """Save profiles configuration to file with secure permissions"""
283
+ self._ensure_global_config_dir()
284
+
285
+ # Write to temp file first, then rename for atomic operation
286
+ temp_file = self.profiles_file.with_suffix(".tmp")
287
+ with open(temp_file, "w") as f:
288
+ json.dump(profiles_config.model_dump(exclude_none=True), f, indent=2)
289
+
290
+ # Set secure permissions (only user can read/write)
291
+ temp_file.chmod(0o600)
292
+
293
+ # Atomic rename
294
+ temp_file.rename(self.profiles_file)
295
+
296
+ def get_profile(self, profile_name: str) -> ProfileData | None:
297
+ """Get profile data by name"""
298
+ profiles_config = self.load_profiles()
299
+ return profiles_config.profiles.get(profile_name)
300
+
301
+ def set_profile(
302
+ self, profile_name: str, profile_data: ProfileData, token: str | None = None
303
+ ) -> None:
304
+ """Set or update a profile"""
305
+ profiles_config = self.load_profiles()
306
+ profiles_config.profiles[profile_name] = profile_data
307
+ self.save_profiles(profiles_config)
308
+
309
+ # Store token in keyring if provided
310
+ if not token or self._store_token_in_keyring(profile_name, token):
311
+ return
312
+
313
+ if self._is_keyring_enabled():
314
+ raise ValueError(
315
+ "Failed to store token in keyring. "
316
+ "Please check your system keyring setup."
317
+ )
318
+ else:
319
+ raise ValueError(
320
+ "Keyring is disabled. "
321
+ "Please set WORKATO_API_TOKEN environment variable instead."
322
+ )
323
+
324
+ def delete_profile(self, profile_name: str) -> bool:
325
+ """Delete a profile by name"""
326
+ profiles_config = self.load_profiles()
327
+ if profile_name not in profiles_config.profiles:
328
+ return False
329
+
330
+ del profiles_config.profiles[profile_name]
331
+
332
+ # If this was the current profile, clear it
333
+ if profiles_config.current_profile == profile_name:
334
+ profiles_config.current_profile = None
335
+
336
+ # Delete token from keyring
337
+ self._delete_token_from_keyring(profile_name)
338
+
339
+ self.save_profiles(profiles_config)
340
+ return True
341
+
342
+ def get_current_profile_name(
343
+ self, project_profile_override: str | None = None
344
+ ) -> str | None:
345
+ """Get current profile name, considering project override"""
346
+ # Priority order:
347
+ # 1. Project-specific profile override
348
+ # 2. Environment variable WORKATO_PROFILE
349
+ # 3. Global current profile setting
350
+
351
+ if project_profile_override:
352
+ return project_profile_override
353
+
354
+ env_profile = os.environ.get("WORKATO_PROFILE")
355
+ if env_profile:
356
+ return env_profile
357
+
358
+ profiles_config = self.load_profiles()
359
+ return profiles_config.current_profile
360
+
361
+ def set_current_profile(self, profile_name: str | None) -> None:
362
+ """Set the current profile in global config"""
363
+ profiles_config = self.load_profiles()
364
+ profiles_config.current_profile = profile_name
365
+ self.save_profiles(profiles_config)
366
+
367
+ def get_current_profile_data(
368
+ self, project_profile_override: str | None = None
369
+ ) -> ProfileData | None:
370
+ """Get current profile data"""
371
+ profile_name = self.get_current_profile_name(project_profile_override)
372
+ if not profile_name:
373
+ return None
374
+ return self.get_profile(profile_name)
375
+
376
+ def list_profiles(self) -> dict[str, ProfileData]:
377
+ """Get all available profiles"""
378
+ profiles_config = self.load_profiles()
379
+ return profiles_config.profiles.copy()
380
+
381
+ def resolve_environment_variables(
382
+ self, project_profile_override: str | None = None
383
+ ) -> tuple[str | None, str | None]:
384
+ """Resolve API token and host with environment variable override support"""
385
+ # Check for environment variable overrides first (highest priority)
386
+ env_token = os.environ.get("WORKATO_API_TOKEN")
387
+ env_host = os.environ.get("WORKATO_HOST")
388
+
389
+ if env_token and env_host:
390
+ return env_token, env_host
391
+
392
+ # Fall back to profile-based configuration
393
+ profile_name = self.get_current_profile_name(project_profile_override)
394
+ if not profile_name:
395
+ return None, None
396
+
397
+ profile_data = self.get_profile(profile_name)
398
+ if not profile_data:
399
+ return None, None
400
+
401
+ # Get token from keyring or env var, use profile data for host
402
+ api_token = env_token or self._get_token_from_keyring(profile_name)
403
+ api_host = env_host or profile_data.region_url
404
+
405
+ return api_token, api_host
406
+
407
+ def validate_credentials(
408
+ self, project_profile_override: str | None = None
409
+ ) -> tuple[bool, list[str]]:
410
+ """Validate that credentials are available from environment or profile"""
411
+ api_token, api_host = self.resolve_environment_variables(
412
+ project_profile_override
413
+ )
414
+ missing_items = []
415
+
416
+ if not api_token:
417
+ missing_items.append("API token (WORKATO_API_TOKEN or profile credentials)")
418
+ if not api_host:
419
+ missing_items.append("API host (WORKATO_HOST or profile region)")
420
+
421
+ return len(missing_items) == 0, missing_items
422
+
423
+ async def select_region_interactive(
424
+ self, profile_name: str | None = None
425
+ ) -> RegionInfo | None:
426
+ """Interactive region selection"""
427
+ regions = list(AVAILABLE_REGIONS.values())
428
+
429
+ click.echo()
430
+
431
+ # Create choices for inquirer
432
+ choices = []
433
+ for region in regions:
434
+ if region.region == "custom":
435
+ choice_text = "Custom URL"
436
+ else:
437
+ choice_text = f"{region.name} ({region.url})"
438
+
439
+ choices.append(choice_text)
440
+
441
+ questions = [
442
+ inquirer.List(
443
+ "region",
444
+ message="Select your Workato region",
445
+ choices=choices,
446
+ ),
447
+ ]
448
+
449
+ answers = inquirer.prompt(questions)
450
+ if not answers: # User cancelled
451
+ return None
452
+
453
+ # Find the selected region by index
454
+ selected_choice = answers["region"]
455
+ selected_index = choices.index(selected_choice)
456
+ selected_region = regions[selected_index]
457
+
458
+ # Handle custom URL
459
+ if selected_region.region == "custom":
460
+ click.echo()
461
+
462
+ # Get selected profile's custom URL as default
463
+ profile_data = None
464
+ if profile_name:
465
+ profile_data = self.get_profile(profile_name)
466
+ else:
467
+ profile_data = self.get_current_profile_data()
468
+
469
+ current_url = "https://www.workato.com" # fallback default
470
+ if profile_data and profile_data.region == "custom":
471
+ current_url = profile_data.region_url
472
+
473
+ custom_url = await click.prompt(
474
+ "Enter your custom Workato base URL",
475
+ type=str,
476
+ default=current_url,
477
+ )
478
+
479
+ # Validate URL security
480
+ is_valid, error_msg = _validate_url_security(custom_url)
481
+ if not is_valid:
482
+ click.echo(f"❌ {error_msg}")
483
+ return None
484
+
485
+ # Parse URL and keep only scheme + netloc (strip any path components)
486
+ parsed = urlparse(custom_url)
487
+ custom_url = f"{parsed.scheme}://{parsed.netloc}"
488
+
489
+ return RegionInfo(region="custom", name="Custom URL", url=custom_url)
490
+
491
+ return selected_region
@@ -0,0 +1,113 @@
1
+ """Workspace and project directory management."""
2
+
3
+ import json
4
+ import sys
5
+
6
+ from pathlib import Path
7
+
8
+ import asyncclick as click
9
+
10
+
11
+ class WorkspaceManager:
12
+ """Manages workspace root detection and validation"""
13
+
14
+ def __init__(self, start_path: Path | None = None):
15
+ self.start_path = start_path or Path.cwd()
16
+
17
+ def find_nearest_workatoenv(self) -> Path | None:
18
+ """Find the nearest .workatoenv file by traversing up the directory tree"""
19
+ current = self.start_path.resolve()
20
+
21
+ while current != current.parent:
22
+ workatoenv_file = current / ".workatoenv"
23
+ if workatoenv_file.exists():
24
+ return current
25
+ current = current.parent
26
+
27
+ return None
28
+
29
+ def find_workspace_root(self) -> Path:
30
+ """Find workspace root by traversing up for .workatoenv file
31
+ with project_path"""
32
+ current = self.start_path.resolve()
33
+
34
+ while current != current.parent:
35
+ workatoenv_file = current / ".workatoenv"
36
+ if workatoenv_file.exists():
37
+ try:
38
+ with open(workatoenv_file) as f:
39
+ data = json.load(f)
40
+ # Workspace root has project_path pointing to a project
41
+ if "project_path" in data and data["project_path"]:
42
+ return current
43
+ # If no project_path, this might be a project directory itself
44
+ elif "project_id" in data and not data.get("project_path"):
45
+ # This is a project directory, continue searching up
46
+ pass
47
+ except (json.JSONDecodeError, OSError):
48
+ pass
49
+ current = current.parent
50
+
51
+ # If no workspace found, current directory becomes workspace root
52
+ return self.start_path
53
+
54
+ def is_in_project_directory(self) -> bool:
55
+ """Check if current directory is a project directory"""
56
+ workatoenv_file = self.start_path / ".workatoenv"
57
+ if not workatoenv_file.exists():
58
+ return False
59
+
60
+ try:
61
+ with open(workatoenv_file) as f:
62
+ data = json.load(f)
63
+ # Project directory has project_id but no project_path
64
+ return "project_id" in data and not data.get("project_path")
65
+ except (json.JSONDecodeError, OSError):
66
+ return False
67
+
68
+ def validate_not_in_project(self) -> None:
69
+ """Validate that we're not running from within a project directory"""
70
+ if self.is_in_project_directory():
71
+ workspace_root = self.find_workspace_root()
72
+ if workspace_root != self.start_path:
73
+ click.echo(f"❌ Run init from workspace root: {workspace_root}")
74
+ else:
75
+ click.echo("❌ Cannot run init from within a project directory")
76
+ sys.exit(1)
77
+
78
+ def validate_project_path(self, project_path: Path, workspace_root: Path) -> None:
79
+ """Validate project path follows our rules"""
80
+ # Convert to absolute paths for comparison
81
+ abs_project_path = project_path.resolve()
82
+ abs_workspace_root = workspace_root.resolve()
83
+
84
+ # Project cannot be in workspace root directly
85
+ if abs_project_path == abs_workspace_root:
86
+ raise ValueError(
87
+ "Projects cannot be created in workspace root directly. "
88
+ "Use a subdirectory."
89
+ )
90
+
91
+ # Project must be within workspace
92
+ try:
93
+ abs_project_path.relative_to(abs_workspace_root)
94
+ except ValueError as e:
95
+ raise ValueError(
96
+ f"Project path must be within workspace root: {abs_workspace_root}"
97
+ ) from e
98
+
99
+ # Check for nested projects by looking for .workatoenv in parent directories
100
+ current = abs_project_path.parent
101
+ while current != abs_workspace_root and current != current.parent:
102
+ if (current / ".workatoenv").exists():
103
+ try:
104
+ with open(current / ".workatoenv") as f:
105
+ data = json.load(f)
106
+ if "project_id" in data:
107
+ raise ValueError(
108
+ f"Cannot create project within another "
109
+ f"project: {current}"
110
+ )
111
+ except (json.JSONDecodeError, OSError):
112
+ pass
113
+ current = current.parent