Repository navigation
Expand file tree
/
Copy pathapp.py
More file actions
311 lines (265 loc) · 13.6 KB
/
Copy pathapp.py
File metadata and controls
311 lines (265 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
import os
import io
import datetime
from typing import List, Tuple
from azure.identity import DefaultAzureCredential, ClientSecretCredential
from azure.storage.blob import BlobServiceClient
from azure.core.exceptions import ResourceExistsError
from flask import Flask, flash, jsonify, render_template, request, redirect, url_for
# Initialize Flask application
app: Flask = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", os.urandom(24))
client_id: str | None
client_secret: str | None
tenant_id: str | None
connection_string: str | None = None
account_url: str | None = None
container_name: str | None = None
blob_service_client: BlobServiceClient | None = None
debug: bool = os.environ.get("FLASK_DEBUG", "false").lower() == "true"
activities: List[Tuple[str, str]] = []
# Suffix of the blobs holding the activities, one blob per activity.
ACTIVITY_BLOB_SUFFIX: str = "-activity.txt"
def is_activity_name(name: str | None) -> bool:
"""Whether the name is one of this app's activity blobs, and nothing else.
Every activity is a blob called YYYY-MM-DD-HH-MM-SS-activity.txt, so requiring that shape is both the
read filter and the write guard: it rejects an empty name and any name the app did not create. Names
arrive from a form field, so they are checked before they reach the container.
"""
return bool(name) and name == os.path.basename(str(name)) and str(name).endswith(ACTIVITY_BLOB_SUFFIX)
def get_environment_variables():
"""Get the value of an environment variable or raise an error if not set."""
global connection_string, container_name, client_id, client_secret, tenant_id, account_url
try:
# Get Azure credentials from environment variables
client_id = os.environ.get("AZURE_CLIENT_ID")
client_secret = os.environ.get("AZURE_CLIENT_SECRET")
tenant_id = os.environ.get("AZURE_TENANT_ID")
# Get connection string from environment variable
connection_string = os.environ.get("AZURE_STORAGE_ACCOUNT_CONNECTION_STRING")
account_url = os.environ.get("AZURE_STORAGE_ACCOUNT_URL")
# Get container name from environment variable with a default value
container_name = os.environ.get("CONTAINER_NAME", "activities")
except ValueError as ve:
print(f"Configuration Error: {ve}")
except Exception as ex:
print(f"An error occurred: {ex}")
def get_blob_service_client():
"""Create a BlobServiceClient using the connection string."""
global connection_string
try:
# Create BlobServiceClient
print(f"Creating BlobServiceClient...")
if client_id and client_secret and tenant_id and account_url:
# Use ClientSecretCredential for authentication
print("Using ClientSecretCredential with BlobServiceClient...")
credential = ClientSecretCredential(tenant_id=tenant_id,
client_id=client_id,
client_secret=client_secret)
blob_service_client = BlobServiceClient(account_url=account_url, credential=credential)
elif connection_string:
# Use connection string for authentication
print("Using storage account connection string with BlobServiceClient...")
blob_service_client = BlobServiceClient.from_connection_string(connection_string)
elif account_url:
# Use DefaultAzureCredential for authentication
print("Using DefaultAzureCredential with BlobServiceClient...")
credential = DefaultAzureCredential()
blob_service_client = BlobServiceClient(account_url=account_url, credential=credential)
else:
raise ValueError("Insufficient configuration for BlobServiceClient. Please set the necessary environment variables.")
print("BlobServiceClient created successfully.")
return blob_service_client
except Exception as ex:
print(f"An error occurred while creating BlobServiceClient: {ex}")
return None
def create_container_if_not_exists():
"""Create a container if it does not already exist."""
global blob_service_client, container_name
try:
if not blob_service_client:
raise ValueError("BlobServiceClient is not initialized. Please call get_blob_service_client() first.")
# Create a ContainerClient
if not container_name:
raise ValueError("Container name is not set. Please set the CONTAINER_NAME environment variable.")
# Get the container client
print(f"Creating container client for container: {container_name}")
container_client = blob_service_client.get_container_client(container_name)
# Check whether the container exists and create it if it does not
if not container_client.exists():
print(f"Attempting to create container '{container_name}' if it does not exist.")
container_client.create_container()
print(f"Container '{container_name}' created.")
else:
print(f"Container '{container_name}' already exists.")
except ValueError as ve:
print(f"Configuration Error: {ve}")
except ResourceExistsError:
print(f"Container '{container_name}' already exists.")
except Exception as ex:
print(f"An error occurred while creating the container: {ex}")
def read_blobs_from_container():
"""Read all blobs from the container."""
global blob_service_client, container_name, activities
try:
if not blob_service_client:
raise ValueError("BlobServiceClient is not initialized. Please call get_blob_service_client() first.")
if not container_name:
raise ValueError("Container name is not set. Please set the CONTAINER_NAME environment variable.")
# Get the container client
container_client = blob_service_client.get_container_client(container_name)
# List all blobs in the container
blob_list = container_client.list_blobs()
for blob in blob_list:
if blob.name:
# Print blob details
print(f"Found blob: {blob.name} with size {blob.size} bytes")
# Read blob content
blob_client = container_client.get_blob_client(blob.name)
blob_content = blob_client.download_blob().readall()
if isinstance(blob_content, bytes):
blob_content = blob_content.decode('utf-8')
else:
blob_content = str(blob_content)
print(f"Content of blob '{blob.name}': {blob_content}")
activities.append((blob.name, blob_content))
except ValueError as ve:
print(f"Configuration Error: {ve}")
except Exception as ex:
print(f"An error occurred while reading blobs from the container: {ex}")
def create_blob_if_not_exists(name: str | None, content: str | None):
"""Create a blob in the container if it does not already exist."""
global blob_service_client, container_name
# Check if name and content are provided
if not name or not content:
raise ValueError("Both 'name' and 'content' must be provided to create a blob.")
try:
if not blob_service_client:
raise ValueError("BlobServiceClient is not initialized. Please call get_blob_service_client() first.")
if not container_name:
raise ValueError("Container name is not set. Please set the CONTAINER_NAME environment variable.")
# Get the container client
container_client = blob_service_client.get_container_client(container_name)
# Create a blob client
blob_client = container_client.get_blob_client(name)
# Check if the blob exists and create it if it does not
print(f"Creating blob '{name}' in container '{container_name}'.")
with io.BytesIO(content.encode("utf-8")) as content_stream:
blob_client.upload_blob(content_stream, blob_type="BlockBlob", overwrite=True)
print(f"Blob '{name}' created successfully.")
except ValueError as ve:
print(f"Configuration Error: {ve}")
except ResourceExistsError:
print(f"Blob '{name}' already exists in container '{container_name}'.")
except Exception as ex:
print(f"An error occurred while creating the blob: {ex}")
def delete_blob(name: str):
"""Delete a blob from the container."""
global blob_service_client, container_name
try:
if not blob_service_client:
raise ValueError("BlobServiceClient is not initialized. Please call get_blob_service_client() first.")
if not container_name:
raise ValueError("Container name is not set. Please set the CONTAINER_NAME environment variable.")
# Get the container client
container_client = blob_service_client.get_container_client(container_name)
# Create a blob client
blob_client = container_client.get_blob_client(name)
# Delete the blob
print(f"Deleting blob '{name}' from container '{container_name}'.")
blob_client.delete_blob()
print(f"Blob '{name}' deleted successfully.")
except ValueError as ve:
print(f"Configuration Error: {ve}")
except Exception as ex:
print(f"An error occurred while deleting the blob: {ex}")
def update_blob(name: str, content: str):
"""Update the content of an existing blob (overwrite in place)."""
global blob_service_client, container_name
if not name or not content:
raise ValueError("Both 'name' and 'content' must be provided.")
try:
if not blob_service_client:
raise ValueError("BlobServiceClient is not initialized.")
if not container_name:
raise ValueError("Container name is not set.")
container_client = blob_service_client.get_container_client(container_name)
blob_client = container_client.get_blob_client(name)
print(f"Updating blob '{name}' in container '{container_name}'.")
with io.BytesIO(content.encode("utf-8")) as content_stream:
blob_client.upload_blob(content_stream, blob_type="BlockBlob", overwrite=True)
print(f"Blob '{name}' updated successfully.")
except ValueError as ve:
print(f"Configuration Error: {ve}")
except Exception as ex:
print(f"An error occurred while updating the blob: {ex}")
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
row_id = request.form.get('row_id', '').strip()
activity = request.form.get('activity', '').strip()
if activity:
if row_id:
# Update the existing blob content in place. The name comes from the form, so it is checked
# before it reaches the container.
if not is_activity_name(row_id):
print(f"Invalid activity name '{row_id}'.")
return redirect(url_for('index'))
update_blob(row_id, activity)
for i, act in enumerate(activities):
if act[0] == row_id:
activities[i] = (row_id, activity)
break
flash('Activity updated successfully.')
else:
# Generate a unique blob name with a timestamp
timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
name = f"{timestamp}{ACTIVITY_BLOB_SUFFIX}"
create_blob_if_not_exists(name, activity)
activities.append((name, activity))
flash('Activity added successfully.')
return redirect(url_for('index'))
# Always reload activities from blob storage on GET
activities.clear()
read_blobs_from_container()
return render_template('index.html', activities=activities)
@app.route('/delete/<string:activity_id>', methods=['POST'])
def delete(activity_id: str):
"""Delete the activity whose blob name is activity_id.
The activity is identified by its blob name, never by its position in the rendered page: every replica
reloads the container on each GET, so the list can change between rendering a page and submitting a
delete from it, and a position would then delete whatever activity happens to sit there now.
"""
if not is_activity_name(activity_id):
print(f"Invalid activity name '{activity_id}'.")
return redirect(url_for('index'))
delete_blob(activity_id)
for i, act in enumerate(activities):
if act[0] == activity_id:
activities.pop(i)
break
flash('Activity deleted successfully.')
return redirect(url_for('index'))
@app.route('/health')
def health():
"""Liveness and readiness probe: reports whether the blob container is reachable."""
try:
if not blob_service_client or not container_name:
raise ValueError("BlobServiceClient is not initialized.")
if not blob_service_client.get_container_client(container_name).exists():
raise ValueError(f"Container '{container_name}' does not exist.")
return jsonify({"status": "ok"})
except Exception as ex:
print(f"Health check failed: {ex}")
return jsonify({"status": "unavailable"}), 503
# Initialize the application and Azure services when the module is loaded.
# This ensures that the setup runs regardless of how the app is started (e.g., via 'flask run' or directly).
get_environment_variables()
blob_service_client = get_blob_service_client()
if blob_service_client:
# Create the container if it does not exist
create_container_if_not_exists()
# Read existing blobs from the container, if any
read_blobs_from_container()
if __name__ == '__main__':
app.run(debug=True)