investing-algorithm-framework 7.19.14__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.

Potentially problematic release.


This version of investing-algorithm-framework might be problematic. Click here for more details.

Files changed (260) hide show
  1. investing_algorithm_framework/__init__.py +197 -0
  2. investing_algorithm_framework/app/__init__.py +47 -0
  3. investing_algorithm_framework/app/algorithm/__init__.py +7 -0
  4. investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
  5. investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
  6. investing_algorithm_framework/app/analysis/__init__.py +15 -0
  7. investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
  8. investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
  9. investing_algorithm_framework/app/analysis/permutation.py +116 -0
  10. investing_algorithm_framework/app/analysis/ranking.py +297 -0
  11. investing_algorithm_framework/app/app.py +2204 -0
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1667 -0
  14. investing_algorithm_framework/app/eventloop.py +590 -0
  15. investing_algorithm_framework/app/reporting/__init__.py +27 -0
  16. investing_algorithm_framework/app/reporting/ascii.py +921 -0
  17. investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
  18. investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
  19. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  20. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  21. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
  22. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  23. investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
  24. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  25. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
  26. investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
  27. investing_algorithm_framework/app/reporting/generate.py +185 -0
  28. investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
  29. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
  30. investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
  31. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
  32. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
  33. investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
  34. investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
  35. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
  36. investing_algorithm_framework/app/stateless/__init__.py +35 -0
  37. investing_algorithm_framework/app/stateless/action_handlers/__init__.py +84 -0
  38. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +8 -0
  39. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +15 -0
  40. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +40 -0
  41. investing_algorithm_framework/app/stateless/exception_handler.py +40 -0
  42. investing_algorithm_framework/app/strategy.py +675 -0
  43. investing_algorithm_framework/app/task.py +41 -0
  44. investing_algorithm_framework/app/web/__init__.py +5 -0
  45. investing_algorithm_framework/app/web/controllers/__init__.py +13 -0
  46. investing_algorithm_framework/app/web/controllers/orders.py +20 -0
  47. investing_algorithm_framework/app/web/controllers/portfolio.py +20 -0
  48. investing_algorithm_framework/app/web/controllers/positions.py +18 -0
  49. investing_algorithm_framework/app/web/create_app.py +20 -0
  50. investing_algorithm_framework/app/web/error_handler.py +59 -0
  51. investing_algorithm_framework/app/web/responses.py +20 -0
  52. investing_algorithm_framework/app/web/run_strategies.py +4 -0
  53. investing_algorithm_framework/app/web/schemas/__init__.py +12 -0
  54. investing_algorithm_framework/app/web/schemas/order.py +12 -0
  55. investing_algorithm_framework/app/web/schemas/portfolio.py +22 -0
  56. investing_algorithm_framework/app/web/schemas/position.py +15 -0
  57. investing_algorithm_framework/app/web/setup_cors.py +6 -0
  58. investing_algorithm_framework/cli/__init__.py +0 -0
  59. investing_algorithm_framework/cli/cli.py +207 -0
  60. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +499 -0
  61. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  62. investing_algorithm_framework/cli/initialize_app.py +603 -0
  63. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  64. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  65. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  66. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  67. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  68. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  69. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  70. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  71. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  72. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  73. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  74. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  75. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  76. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  77. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  78. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  79. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  80. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  81. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  82. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  83. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  84. investing_algorithm_framework/create_app.py +54 -0
  85. investing_algorithm_framework/dependency_container.py +155 -0
  86. investing_algorithm_framework/domain/__init__.py +148 -0
  87. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  88. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  89. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  90. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  91. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  92. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  93. investing_algorithm_framework/domain/backtesting/backtest_run.py +435 -0
  94. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  95. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  96. investing_algorithm_framework/domain/config.py +111 -0
  97. investing_algorithm_framework/domain/constants.py +83 -0
  98. investing_algorithm_framework/domain/data_provider.py +334 -0
  99. investing_algorithm_framework/domain/data_structures.py +42 -0
  100. investing_algorithm_framework/domain/decimal_parsing.py +40 -0
  101. investing_algorithm_framework/domain/exceptions.py +112 -0
  102. investing_algorithm_framework/domain/models/__init__.py +43 -0
  103. investing_algorithm_framework/domain/models/app_mode.py +34 -0
  104. investing_algorithm_framework/domain/models/base_model.py +25 -0
  105. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  106. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  107. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  108. investing_algorithm_framework/domain/models/event.py +35 -0
  109. investing_algorithm_framework/domain/models/market/__init__.py +5 -0
  110. investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
  111. investing_algorithm_framework/domain/models/order/__init__.py +6 -0
  112. investing_algorithm_framework/domain/models/order/order.py +384 -0
  113. investing_algorithm_framework/domain/models/order/order_side.py +36 -0
  114. investing_algorithm_framework/domain/models/order/order_status.py +37 -0
  115. investing_algorithm_framework/domain/models/order/order_type.py +30 -0
  116. investing_algorithm_framework/domain/models/portfolio/__init__.py +9 -0
  117. investing_algorithm_framework/domain/models/portfolio/portfolio.py +169 -0
  118. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +93 -0
  119. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
  120. investing_algorithm_framework/domain/models/position/__init__.py +4 -0
  121. investing_algorithm_framework/domain/models/position/position.py +68 -0
  122. investing_algorithm_framework/domain/models/position/position_snapshot.py +47 -0
  123. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  124. investing_algorithm_framework/domain/models/strategy_profile.py +33 -0
  125. investing_algorithm_framework/domain/models/time_frame.py +153 -0
  126. investing_algorithm_framework/domain/models/time_interval.py +124 -0
  127. investing_algorithm_framework/domain/models/time_unit.py +149 -0
  128. investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
  129. investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
  130. investing_algorithm_framework/domain/models/trade/__init__.py +13 -0
  131. investing_algorithm_framework/domain/models/trade/trade.py +388 -0
  132. investing_algorithm_framework/domain/models/trade/trade_risk_type.py +34 -0
  133. investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
  134. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +267 -0
  135. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +303 -0
  136. investing_algorithm_framework/domain/order_executor.py +112 -0
  137. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  138. investing_algorithm_framework/domain/positions/__init__.py +4 -0
  139. investing_algorithm_framework/domain/positions/position_size.py +41 -0
  140. investing_algorithm_framework/domain/services/__init__.py +11 -0
  141. investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
  142. investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
  143. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
  144. investing_algorithm_framework/domain/services/rounding_service.py +27 -0
  145. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  146. investing_algorithm_framework/domain/stateless_actions.py +7 -0
  147. investing_algorithm_framework/domain/strategy.py +44 -0
  148. investing_algorithm_framework/domain/utils/__init__.py +27 -0
  149. investing_algorithm_framework/domain/utils/csv.py +104 -0
  150. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  151. investing_algorithm_framework/domain/utils/dates.py +57 -0
  152. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  153. investing_algorithm_framework/domain/utils/polars.py +53 -0
  154. investing_algorithm_framework/domain/utils/random.py +41 -0
  155. investing_algorithm_framework/domain/utils/signatures.py +17 -0
  156. investing_algorithm_framework/domain/utils/stoppable_thread.py +26 -0
  157. investing_algorithm_framework/domain/utils/synchronized.py +12 -0
  158. investing_algorithm_framework/download_data.py +108 -0
  159. investing_algorithm_framework/infrastructure/__init__.py +50 -0
  160. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  161. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
  162. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  163. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  164. investing_algorithm_framework/infrastructure/database/__init__.py +10 -0
  165. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +120 -0
  166. investing_algorithm_framework/infrastructure/models/__init__.py +16 -0
  167. investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
  168. investing_algorithm_framework/infrastructure/models/model_extension.py +6 -0
  169. investing_algorithm_framework/infrastructure/models/order/__init__.py +4 -0
  170. investing_algorithm_framework/infrastructure/models/order/order.py +124 -0
  171. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  172. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  173. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +4 -0
  174. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
  175. investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py +114 -0
  176. investing_algorithm_framework/infrastructure/models/position/__init__.py +4 -0
  177. investing_algorithm_framework/infrastructure/models/position/position.py +63 -0
  178. investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
  179. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  180. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  181. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +40 -0
  182. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +41 -0
  183. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  184. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  185. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  186. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  187. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  188. investing_algorithm_framework/infrastructure/repositories/__init__.py +21 -0
  189. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  190. investing_algorithm_framework/infrastructure/repositories/order_repository.py +96 -0
  191. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +30 -0
  192. investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
  193. investing_algorithm_framework/infrastructure/repositories/position_repository.py +66 -0
  194. investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
  195. investing_algorithm_framework/infrastructure/repositories/repository.py +299 -0
  196. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  197. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +23 -0
  198. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +23 -0
  199. investing_algorithm_framework/infrastructure/services/__init__.py +7 -0
  200. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  201. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
  202. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  203. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  204. investing_algorithm_framework/services/__init__.py +132 -0
  205. investing_algorithm_framework/services/backtesting/__init__.py +5 -0
  206. investing_algorithm_framework/services/backtesting/backtest_service.py +651 -0
  207. investing_algorithm_framework/services/configuration_service.py +96 -0
  208. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  209. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  210. investing_algorithm_framework/services/market_credential_service.py +40 -0
  211. investing_algorithm_framework/services/metrics/__init__.py +114 -0
  212. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  213. investing_algorithm_framework/services/metrics/beta.py +0 -0
  214. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  215. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  216. investing_algorithm_framework/services/metrics/drawdown.py +181 -0
  217. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  218. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  219. investing_algorithm_framework/services/metrics/generate.py +358 -0
  220. investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
  221. investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
  222. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  223. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  224. investing_algorithm_framework/services/metrics/returns.py +452 -0
  225. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  226. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  227. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  228. investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
  229. investing_algorithm_framework/services/metrics/trades.py +500 -0
  230. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  231. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  232. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  233. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  234. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  235. investing_algorithm_framework/services/order_service/__init__.py +9 -0
  236. investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
  237. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  238. investing_algorithm_framework/services/order_service/order_service.py +826 -0
  239. investing_algorithm_framework/services/portfolios/__init__.py +16 -0
  240. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
  241. investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +75 -0
  242. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  243. investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
  244. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
  245. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
  246. investing_algorithm_framework/services/positions/__init__.py +7 -0
  247. investing_algorithm_framework/services/positions/position_service.py +210 -0
  248. investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
  249. investing_algorithm_framework/services/repository_service.py +40 -0
  250. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  251. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +132 -0
  252. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +66 -0
  253. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +41 -0
  254. investing_algorithm_framework/services/trade_service/__init__.py +3 -0
  255. investing_algorithm_framework/services/trade_service/trade_service.py +1083 -0
  256. investing_algorithm_framework-7.19.14.dist-info/LICENSE +201 -0
  257. investing_algorithm_framework-7.19.14.dist-info/METADATA +459 -0
  258. investing_algorithm_framework-7.19.14.dist-info/RECORD +260 -0
  259. investing_algorithm_framework-7.19.14.dist-info/WHEEL +4 -0
  260. investing_algorithm_framework-7.19.14.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,718 @@
1
+ import os
2
+ import subprocess
3
+ import re
4
+ import random
5
+ import string
6
+ import asyncio
7
+ import time
8
+
9
+ from azure.identity import DefaultAzureCredential
10
+ from azure.mgmt.resource import ResourceManagementClient
11
+ from azure.mgmt.storage import StorageManagementClient
12
+ from azure.mgmt.web import WebSiteManagementClient
13
+
14
+ STORAGE_ACCOUNT_NAME_PREFIX = "iafstorageaccount"
15
+
16
+
17
+ def generate_unique_resource_name(base_name):
18
+ """
19
+ Function to generate a unique resource name by appending a random suffix.
20
+
21
+ Args:
22
+ base_name (str): The base name for the resource.
23
+
24
+ Returns:
25
+ str: The unique resource name.
26
+ """
27
+ unique_suffix = ''.join(
28
+ random.choices(string.ascii_lowercase + string.digits, k=6)
29
+ )
30
+ return f"{base_name}{unique_suffix}".lower()
31
+
32
+
33
+ def ensure_azure_functools():
34
+ """
35
+ Function to ensure that the Azure Functions Core Tools are installed.
36
+ If not, it will prompt the user to install it.
37
+ """
38
+
39
+ try:
40
+ result = subprocess.run(
41
+ ["func", "--version"],
42
+ stdout=subprocess.PIPE,
43
+ stderr=subprocess.PIPE,
44
+ text=True,
45
+ )
46
+ if result.returncode != 0:
47
+ raise FileNotFoundError("Azure Functions Core Tools not found.")
48
+ except FileNotFoundError:
49
+ print("Azure Functions Core Tools not found. Please install it.")
50
+ print("You can install it using the following command:")
51
+ print("npm install -g azure-functions-core-tools@4 --unsafe-perm true")
52
+ exit(1)
53
+
54
+
55
+ async def read_env_file_and_set_function_env_variables(
56
+ function_app_name,
57
+ storage_connection_string,
58
+ storage_container_name,
59
+ resource_group_name
60
+ ):
61
+ """
62
+ Function to read the .env file in the working directory
63
+ and set the environment variables for the Function App.
64
+
65
+ Returns:
66
+ None
67
+ """
68
+ env_file_path = os.path.join(os.getcwd(), ".env")
69
+ entries = {}
70
+ if os.path.exists(env_file_path):
71
+ with open(env_file_path, "r") as file:
72
+ for line in file:
73
+ if "=" in line:
74
+ key, value = line.strip().split("=", 1)
75
+ entries[key] = value
76
+
77
+ # Convert dictionary to CLI format
78
+ settings = [f"{key}={value}" for key, value in entries.items()]
79
+
80
+ # Construct the command
81
+ command = [
82
+ "az", "functionapp", "config", "appsettings", "set",
83
+ "--name", function_app_name,
84
+ "--resource-group", resource_group_name,
85
+ "--settings"
86
+ ] + settings # Append all settings
87
+
88
+ # Run the Azure CLI command asynchronously
89
+ process = await asyncio.create_subprocess_exec(
90
+ *command,
91
+ stdout=asyncio.subprocess.PIPE,
92
+ stderr=asyncio.subprocess.PIPE
93
+ )
94
+
95
+ stdout, stderr = await process.communicate()
96
+
97
+ if process.returncode == 0:
98
+ print(
99
+ "Environment variables successfully set for the function app."
100
+ )
101
+ else:
102
+ print(
103
+ "Error setting environment variables: " +
104
+ f"{stderr.decode().strip()}"
105
+ )
106
+
107
+ else:
108
+ print(f".env file not found at {env_file_path}")
109
+
110
+
111
+ async def publish_function_app(
112
+ function_app_name,
113
+ storage_connection_string,
114
+ storage_container_name,
115
+ resource_group_name
116
+ ):
117
+ """
118
+ Function to publish the Function App using Azure Functions Core Tools.
119
+
120
+ Args:
121
+ function_app_name (str): Name of the Function App to publish.
122
+ storage_connection_string (str): Azure Storage Connection String.
123
+ storage_container_name (str): Azure Storage Container Name.
124
+ resource_group_name (str): Resource Group Name.
125
+
126
+ Returns:
127
+ None
128
+ """
129
+ print(f"Publishing Function App {function_app_name}")
130
+
131
+ # Wait for 60 seconds to ensure the Function App is ready
132
+ time.sleep(60)
133
+
134
+ try:
135
+ # Step 1: Publish the Azure Function App
136
+ process = await asyncio.create_subprocess_exec(
137
+ "func", "azure", "functionapp", "publish", function_app_name
138
+ )
139
+
140
+ # Wait for the subprocess to finish
141
+ _, stderr = await process.communicate()
142
+
143
+ # Check the return code
144
+ if process.returncode != 0:
145
+
146
+ if stderr is not None:
147
+ raise Exception(
148
+ f"Error publishing Function App: {stderr.decode().strip()}"
149
+ )
150
+ else:
151
+ raise Exception("Error publishing Function App")
152
+
153
+ print(f"Function App {function_app_name} published successfully.")
154
+
155
+ # Step 2: Add app settings
156
+ add_settings_process = await asyncio.create_subprocess_exec(
157
+ "az", "functionapp", "config", "appsettings", "set",
158
+ "--name", function_app_name,
159
+ "--settings",
160
+ f"AZURE_STORAGE_CONNECTION_STRING={storage_connection_string}",
161
+ f"AZURE_STORAGE_CONTAINER_NAME={storage_container_name}",
162
+ "--resource-group", resource_group_name
163
+ )
164
+ _, stderr1 = await add_settings_process.communicate()
165
+
166
+ if add_settings_process.returncode != 0:
167
+
168
+ if stderr1 is not None:
169
+ raise Exception(
170
+ f"Error adding App settings: {stderr1.decode().strip()}"
171
+ )
172
+ else:
173
+ raise Exception("Error adding App settings")
174
+
175
+ print(
176
+ "Added app settings to the Function App successfully"
177
+ )
178
+
179
+ # Step 3: Update the cors settings
180
+ cors_process = await asyncio.create_subprocess_exec(
181
+ "az", "functionapp", "cors", "add",
182
+ "--name", function_app_name,
183
+ "--allowed-origins", "*",
184
+ "--resource-group", resource_group_name
185
+ )
186
+
187
+ _, stderr1 = await add_settings_process.communicate()
188
+
189
+ if cors_process.returncode != 0:
190
+
191
+ if stderr1 is not None:
192
+ raise Exception(
193
+ f"Error adding cors settings: {stderr1.decode().strip()}"
194
+ )
195
+ else:
196
+ raise Exception("Error adding cors settings")
197
+
198
+ print("All app settings have been added successfully.")
199
+ print("Function App creation completed successfully.")
200
+ except Exception as e:
201
+ print(f"Error publishing Function App: {e}")
202
+
203
+
204
+ async def create_function_app(
205
+ resource_group_name,
206
+ deployment_name,
207
+ storage_account_name,
208
+ region
209
+ ):
210
+ """
211
+ Creates an Azure Function App in a Consumption Plan and deploys
212
+ a Python Function.
213
+
214
+ Args:
215
+ resource_group_name (str): Resource group name.
216
+ deployment_name (str): Name of the Function App to create.
217
+ storage_account_name (str): Name of the associated Storage Account.
218
+ region (str): Azure region (e.g., "eastus").
219
+
220
+ Returns:
221
+ dict: Details of the created or existing Function App.
222
+ """
223
+ # Check if the Function App already exists
224
+ print(f"Checking if Function App '{deployment_name}' exists...")
225
+
226
+ try:
227
+ # Check for the Function App
228
+ check_process = await asyncio.create_subprocess_exec(
229
+ "az",
230
+ "functionapp",
231
+ "show",
232
+ "--name",
233
+ deployment_name,
234
+ "--resource-group",
235
+ resource_group_name,
236
+ stdout=asyncio.subprocess.PIPE,
237
+ stderr=asyncio.subprocess.PIPE
238
+ )
239
+ stdout, stderr = await check_process.communicate()
240
+
241
+ if check_process.returncode == 0:
242
+ # The Function App exists, return details
243
+ print(f"Function App '{deployment_name}' already exists.")
244
+ return stdout.decode()
245
+
246
+ # If the return code is non-zero, and the error indicates
247
+ # the Function App doesn't exist, proceed to create it
248
+ if "ResourceNotFound" in stderr.decode():
249
+ print(
250
+ f"Function App '{deployment_name}' does not exist." +
251
+ " Proceeding to create it..."
252
+ )
253
+ else:
254
+ # If the error is something else, raise it
255
+ print(f"Error checking for Function App: {stderr.decode()}")
256
+ raise Exception(stderr.decode())
257
+
258
+ # Create the Function App
259
+ print(f"Creating Function App '{deployment_name}'...")
260
+ create_process = await asyncio.create_subprocess_exec(
261
+ "az",
262
+ "functionapp",
263
+ "create",
264
+ "--resource-group",
265
+ resource_group_name,
266
+ "--consumption-plan-location",
267
+ region,
268
+ "--runtime",
269
+ "python",
270
+ "--runtime-version",
271
+ "3.10",
272
+ "--functions-version",
273
+ "4",
274
+ "--name",
275
+ deployment_name,
276
+ "--os-type",
277
+ "linux",
278
+ "--storage-account",
279
+ storage_account_name
280
+ )
281
+
282
+ # Wait for the subprocess to finish
283
+ _, create_stderr = await create_process.communicate()
284
+
285
+ # Check the return code for the create command
286
+ if create_process.returncode != 0:
287
+ print(
288
+ "Error creating Function App: " +
289
+ f"{create_stderr.decode().strip()}"
290
+ )
291
+ raise Exception(
292
+ "Error creating Function App: " +
293
+ f"{create_stderr.decode().strip()}"
294
+ )
295
+
296
+ print(f"Function App '{deployment_name}' created successfully.")
297
+ return {"status": "created"}
298
+
299
+ except Exception as e:
300
+ print(f"Error creating Function App: {e}")
301
+ raise e
302
+
303
+
304
+ def create_file_from_template(template_path, output_path):
305
+ """
306
+ Creates a new file by replacing placeholders in a template file.
307
+
308
+ Args:
309
+ template_path (str): The path to the template file.
310
+ output_path (str): The path to the output file.
311
+ replacements (dict): A dictionary of placeholder
312
+ keys and their replacements.
313
+
314
+ Returns:
315
+ None
316
+ """
317
+ with open(template_path, "r") as file:
318
+ template = file.read()
319
+
320
+ with open(output_path, "w") as file:
321
+ file.write(template)
322
+
323
+
324
+ def ensure_consumption_plan(
325
+ resource_group_name,
326
+ plan_name,
327
+ region,
328
+ subscription_id,
329
+ credential
330
+ ):
331
+ """
332
+ Ensures that an App Service Plan with the Consumption Plan exists.
333
+ If not, creates it.
334
+
335
+ Args:
336
+ resource_group_name (str): The name of the resource group.
337
+ plan_name (str): The name of the App Service Plan.
338
+ region (str): The Azure region for the resources.
339
+ subscription_id (str): The Azure subscription ID.
340
+
341
+ Returns:
342
+ object: The App Service Plan object.
343
+ """
344
+ web_client = WebSiteManagementClient(credential, subscription_id)
345
+
346
+ try:
347
+ print(
348
+ f"Checking if App Service Plan '{plan_name}' exists" +
349
+ f" in resource group '{resource_group_name}'..."
350
+ )
351
+ plan = web_client.app_service_plans.get(resource_group_name, plan_name)
352
+ print(f"App Service Plan '{plan_name}' already exists.")
353
+ except Exception: # Plan does not exist
354
+ print(
355
+ f"App Service Plan '{plan_name}' not found. " +
356
+ "Creating it as a Consumption Plan..."
357
+ )
358
+ plan = web_client.app_service_plans.begin_create_or_update(
359
+ resource_group_name,
360
+ plan_name,
361
+ {
362
+ "location": region,
363
+ "sku": {"name": "Y1", "tier": "Dynamic"},
364
+ "kind": "functionapp", # Mark this as for Function Apps
365
+ "properties": {}
366
+ },
367
+ ).result()
368
+ print(f"App Service Plan '{plan_name}' created successfully.")
369
+ return plan
370
+
371
+
372
+ def ensure_storage_account(
373
+ storage_account_name,
374
+ resource_group_name,
375
+ region,
376
+ subscription_id,
377
+ credential,
378
+ ):
379
+ """
380
+ Checks if a storage account exists. If it doesn't, creates it.
381
+
382
+ If no storage account name is provided, a unique name will
383
+ be generated. However, before we create a new
384
+ storage account, we check if there a storage account exists
385
+ with the prefix 'iafstorageaccount'. If it exists, we use
386
+ that storage account.
387
+
388
+ Args:
389
+ storage_account_name (str): The name of the storage account.
390
+ resource_group_name (str): The name of the resource group.
391
+ region (str): The Azure region for the resources.
392
+ subscription_id (str): The Azure subscription ID.
393
+ credential: Azure credentials object.
394
+
395
+ Returns:
396
+ StorageAccount: The created storage account object.
397
+ """
398
+ # Create Storage Management Client
399
+ storage_client = StorageManagementClient(credential, subscription_id)
400
+
401
+ # Check if the storage account exists
402
+ try:
403
+
404
+ # Check if provided storage account name has prefix 'iafstorageaccount'
405
+ if storage_account_name.startswith(STORAGE_ACCOUNT_NAME_PREFIX):
406
+ # List all storage accounts in the resource group
407
+ storage_accounts = storage_client\
408
+ .storage_accounts.list_by_resource_group(
409
+ resource_group_name
410
+ )
411
+
412
+ for account in storage_accounts:
413
+ if account.name.startswith(STORAGE_ACCOUNT_NAME_PREFIX):
414
+ storage_account_name = account.name
415
+ break
416
+
417
+ storage_client.storage_accounts.get_properties(
418
+ resource_group_name,
419
+ storage_account_name,
420
+ )
421
+ print(f"Storage account '{storage_account_name}' already exists.")
422
+ account_key = storage_client.storage_accounts.list_keys(
423
+ resource_group_name,
424
+ storage_account_name,
425
+ ).keys[1].value
426
+ connection_string = "DefaultEndpointsProtocol=https;" + \
427
+ f"AccountName={storage_account_name};" + \
428
+ f"AccountKey={account_key};EndpointSuffix=core.windows.net"
429
+ return connection_string, storage_account_name
430
+ except Exception: # If the storage account does not exist
431
+ print("Creating storage account ...")
432
+
433
+ # Create storage account
434
+ storage_async_operation = storage_client.storage_accounts.begin_create(
435
+ resource_group_name,
436
+ storage_account_name,
437
+ {
438
+ "location": region,
439
+ "sku": {"name": "Standard_LRS"},
440
+ "kind": "StorageV2",
441
+ },
442
+ )
443
+ storage_async_operation.result()
444
+
445
+ if storage_async_operation.status() == "Succeeded":
446
+ print(
447
+ f"Storage account '{storage_account_name}'" +
448
+ "created successfully."
449
+ )
450
+
451
+ account_key = storage_client.storage_accounts\
452
+ .list_keys(
453
+ resource_group_name,
454
+ storage_account_name,
455
+ ).keys[1].value
456
+ connection_string = f"DefaultEndpointsProtocol=https;"\
457
+ f"AccountName={storage_account_name};" + \
458
+ f"AccountKey={account_key};" + \
459
+ "EndpointSuffix=core.windows.net"
460
+ return connection_string, storage_account_name
461
+
462
+
463
+ def ensure_az_login(skip_check=False):
464
+ """
465
+ Ensures the user is logged into Azure using `az login`.
466
+ If not logged in, it will prompt the user to log in.
467
+
468
+ Raises:
469
+ Exception: An error occurred during the login process.
470
+ """
471
+
472
+ if skip_check:
473
+ return
474
+
475
+ result = subprocess.run(["az", "login"], check=True)
476
+
477
+ if result.returncode != 0:
478
+ raise Exception("An error occurred during 'az login'.")
479
+
480
+
481
+ def get_default_subscription_id():
482
+ """
483
+ Fetches the default subscription ID using Azure CLI.
484
+
485
+ Returns:
486
+ str: The default subscription ID.
487
+ """
488
+ print("Fetching default subscription ID...")
489
+
490
+ # Check if an default subscription ID is set in the environment
491
+ if "AZURE_SUBSCRIPTION_ID" in os.environ:
492
+ return os.environ["AZURE_SUBSCRIPTION_ID"]
493
+
494
+ try:
495
+ print(
496
+ "If you want to use a different subscription, please provide the"
497
+ " subscription ID with the '--subscription_id' option or"
498
+ " by setting the 'AZURE_SUBSCRIPTION_ID' environment variable."
499
+ )
500
+ result = subprocess.run(
501
+ ["az", "account", "show", "--query", "id", "-o", "tsv"],
502
+ stdout=subprocess.PIPE,
503
+ stderr=subprocess.PIPE,
504
+ text=True,
505
+ check=True,
506
+ )
507
+ subscription_id = result.stdout.strip()
508
+ print(f"Default subscription ID: {subscription_id}")
509
+ return subscription_id
510
+ except subprocess.CalledProcessError:
511
+ print(
512
+ "Error fetching default subscription ID." +
513
+ " Please log in with 'az login'."
514
+ )
515
+ raise
516
+
517
+
518
+ def ensure_resource_group(
519
+ resource_group_name,
520
+ region,
521
+ subscription_id,
522
+ create_if_not_exists
523
+ ):
524
+ """
525
+ Checks if a resource group exists. If it doesn't,
526
+ creates it if `create_if_not_exists` is True.
527
+
528
+ Args:
529
+ resource_group_name (str): The name of the resource group.
530
+ region (str): The Azure region for the resources.
531
+ subscription_id (str): The Azure subscription ID.
532
+ create_if_not_exists (bool): Flag to create the
533
+ resource group if it does not exist.
534
+
535
+ Returns:
536
+ None
537
+ """
538
+ credential = DefaultAzureCredential()
539
+ resource_client = ResourceManagementClient(credential, subscription_id)
540
+
541
+ print(f"Checking if resource group '{resource_group_name}' exists...")
542
+ try:
543
+ resource_client.resource_groups.get(resource_group_name)
544
+ print(f"Resource group '{resource_group_name}' already exists.")
545
+ except Exception: # If the resource group does not exist
546
+
547
+ try:
548
+ if create_if_not_exists:
549
+ print(
550
+ f"Resource group '{resource_group_name}' not" +
551
+ " found. Creating it..."
552
+ )
553
+ resource_client.resource_groups.create_or_update(
554
+ resource_group_name,
555
+ {"location": region},
556
+ )
557
+ print(
558
+ f"Resource group '{resource_group_name}'" +
559
+ " created successfully."
560
+ )
561
+ else:
562
+ print(
563
+ f"Resource group '{resource_group_name}' does" +
564
+ " not exist, and 'create_if_not_exists' is False."
565
+ )
566
+ raise ValueError(
567
+ f"Resource group '{resource_group_name}' does not exist."
568
+ )
569
+ except Exception as e:
570
+ raise Exception(f"Error creating resource group: {e}")
571
+
572
+
573
+ def create_storage_and_function(
574
+ resource_group_name,
575
+ storage_account_name,
576
+ container_name,
577
+ deployment_name,
578
+ region,
579
+ subscription_id=None,
580
+ create_resource_group_if_not_exists=False,
581
+ skip_login=False
582
+ ):
583
+
584
+ # Make sure that the deployment name only contains lowercase letters, and
585
+ # uppercase letters
586
+ regex = r"^[a-zA-Z0-9]+$"
587
+ if not re.match(regex, deployment_name):
588
+ raise ValueError(
589
+ "--deployment_name can only contain " +
590
+ "letters (uppercase and lowercase)."
591
+ )
592
+ # Get current working directory
593
+ cwd = os.getcwd()
594
+
595
+ # Get the path of this script (command.py)
596
+ current_script_path = os.path.abspath(__file__)
597
+
598
+ # Construct the path to the template file
599
+ template_host_file_path = os.path.join(
600
+ os.path.dirname(current_script_path),
601
+ "templates",
602
+ "azure_function_host.json.template"
603
+ )
604
+ template_settings_path = os.path.join(
605
+ os.path.dirname(current_script_path),
606
+ "templates",
607
+ "azure_function_local.settings.json.template"
608
+ )
609
+
610
+ create_file_from_template(
611
+ template_host_file_path, os.path.join(cwd, "host.json")
612
+ )
613
+
614
+ create_file_from_template(
615
+ template_settings_path, os.path.join(cwd, "local.settings.json")
616
+ )
617
+
618
+ # Fetch default subscription ID if not provided
619
+ if not subscription_id:
620
+ subscription_id = get_default_subscription_id()
621
+
622
+ # Authenticate using DefaultAzureCredential
623
+ # (requires environment variables or Azure CLI login)
624
+ credential = DefaultAzureCredential()
625
+
626
+ # Check if the resource group exists
627
+ ensure_resource_group(
628
+ resource_group_name,
629
+ region,
630
+ subscription_id,
631
+ create_resource_group_if_not_exists
632
+ )
633
+
634
+ if storage_account_name is None:
635
+ storage_account_name = \
636
+ generate_unique_resource_name(STORAGE_ACCOUNT_NAME_PREFIX)
637
+
638
+ # Ensure storage account exists
639
+ storage_account_connection_string, storage_account_name = \
640
+ ensure_storage_account(
641
+ storage_account_name,
642
+ resource_group_name,
643
+ region,
644
+ subscription_id,
645
+ credential
646
+ )
647
+
648
+ # Create Function App
649
+ asyncio.run(
650
+ create_function_app(
651
+ resource_group_name=resource_group_name,
652
+ deployment_name=deployment_name,
653
+ region=region,
654
+ storage_account_name=storage_account_name
655
+ )
656
+ )
657
+
658
+ # Publish Function App
659
+ asyncio.run(
660
+ publish_function_app(
661
+ function_app_name=deployment_name,
662
+ storage_connection_string=storage_account_connection_string,
663
+ storage_container_name=container_name,
664
+ resource_group_name=resource_group_name
665
+ )
666
+ )
667
+
668
+ print(
669
+ f"Function App '{deployment_name}' deployment" +
670
+ "completed successfully."
671
+ )
672
+
673
+
674
+ def command(
675
+ resource_group,
676
+ subscription_id,
677
+ storage_account_name,
678
+ container_name,
679
+ deployment_name,
680
+ region,
681
+ create_resource_group_if_not_exists,
682
+ skip_login
683
+ ):
684
+ """
685
+ Command-line tool for creating an Azure storage account,
686
+ blob container, and Function App.
687
+
688
+ Args:
689
+ resource_group (str): The name of the resource group.
690
+ subscription_id (str): The Azure subscription ID.
691
+ storage_account_name (str): The name of the storage account.
692
+ container_name (str): The name of the blob container.
693
+ function_app (str): The name of the Azure Function App.
694
+ region (str): The Azure region for the resources.
695
+ create_resource_group_if_not_exists (bool): Flag to create
696
+ the resource group if it does not exist.
697
+
698
+ Returns:
699
+ None
700
+ """
701
+
702
+ print("logging in to Azure...")
703
+ # Ensure the user is logged in
704
+ ensure_az_login(skip_check=skip_login)
705
+
706
+ print("Checking functools...")
707
+ # Ensure azure functions core tools are installed
708
+ ensure_azure_functools()
709
+ create_storage_and_function(
710
+ resource_group_name=resource_group,
711
+ storage_account_name=storage_account_name,
712
+ container_name=container_name,
713
+ deployment_name=deployment_name,
714
+ region=region,
715
+ subscription_id=subscription_id,
716
+ skip_login=skip_login,
717
+ create_resource_group_if_not_exists=create_resource_group_if_not_exists
718
+ )