planar 0.9.3__py3-none-any.whl → 0.11.0__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.
- planar/ai/agent.py +2 -1
- planar/ai/agent_base.py +24 -5
- planar/ai/state.py +17 -0
- planar/app.py +18 -1
- planar/data/connection.py +108 -0
- planar/data/dataset.py +11 -104
- planar/data/utils.py +89 -0
- planar/db/alembic/env.py +25 -1
- planar/files/storage/azure_blob.py +1 -1
- planar/registry_items.py +2 -0
- planar/routers/dataset_router.py +213 -0
- planar/routers/info.py +79 -36
- planar/routers/models.py +1 -0
- planar/routers/workflow.py +2 -0
- planar/scaffold_templates/pyproject.toml.j2 +1 -1
- planar/security/authorization.py +31 -3
- planar/security/default_policies.cedar +25 -0
- planar/testing/fixtures.py +34 -1
- planar/testing/planar_test_client.py +1 -1
- planar/workflows/decorators.py +2 -1
- planar/workflows/wrappers.py +1 -0
- {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/METADATA +9 -1
- {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/RECORD +25 -72
- {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/WHEEL +1 -1
- planar/ai/test_agent_serialization.py +0 -229
- planar/ai/test_agent_tool_step_display.py +0 -78
- planar/data/test_dataset.py +0 -354
- planar/files/storage/test_azure_blob.py +0 -435
- planar/files/storage/test_local_directory.py +0 -162
- planar/files/storage/test_s3.py +0 -299
- planar/files/test_files.py +0 -282
- planar/human/test_human.py +0 -385
- planar/logging/test_formatter.py +0 -327
- planar/modeling/mixins/test_auditable.py +0 -97
- planar/modeling/mixins/test_timestamp.py +0 -134
- planar/modeling/mixins/test_uuid_primary_key.py +0 -52
- planar/routers/test_agents_router.py +0 -174
- planar/routers/test_files_router.py +0 -49
- planar/routers/test_object_config_router.py +0 -367
- planar/routers/test_routes_security.py +0 -168
- planar/routers/test_rule_router.py +0 -470
- planar/routers/test_workflow_router.py +0 -539
- planar/rules/test_data/account_dormancy_management.json +0 -223
- planar/rules/test_data/airline_loyalty_points_calculator.json +0 -262
- planar/rules/test_data/applicant_risk_assessment.json +0 -435
- planar/rules/test_data/booking_fraud_detection.json +0 -407
- planar/rules/test_data/cellular_data_rollover_system.json +0 -258
- planar/rules/test_data/clinical_trial_eligibility_screener.json +0 -437
- planar/rules/test_data/customer_lifetime_value.json +0 -143
- planar/rules/test_data/import_duties_calculator.json +0 -289
- planar/rules/test_data/insurance_prior_authorization.json +0 -443
- planar/rules/test_data/online_check_in_eligibility_system.json +0 -254
- planar/rules/test_data/order_consolidation_system.json +0 -375
- planar/rules/test_data/portfolio_risk_monitor.json +0 -471
- planar/rules/test_data/supply_chain_risk.json +0 -253
- planar/rules/test_data/warehouse_cross_docking.json +0 -237
- planar/rules/test_rules.py +0 -1494
- planar/security/tests/test_auth_middleware.py +0 -162
- planar/security/tests/test_authorization_context.py +0 -78
- planar/security/tests/test_cedar_basics.py +0 -41
- planar/security/tests/test_cedar_policies.py +0 -158
- planar/security/tests/test_jwt_principal_context.py +0 -179
- planar/test_app.py +0 -142
- planar/test_cli.py +0 -394
- planar/test_config.py +0 -515
- planar/test_object_config.py +0 -527
- planar/test_object_registry.py +0 -14
- planar/test_sqlalchemy.py +0 -193
- planar/test_utils.py +0 -105
- planar/testing/test_memory_storage.py +0 -143
- planar/workflows/test_concurrency_detection.py +0 -120
- planar/workflows/test_lock_timeout.py +0 -140
- planar/workflows/test_serialization.py +0 -1203
- planar/workflows/test_suspend_deserialization.py +0 -231
- planar/workflows/test_workflow.py +0 -2005
- {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/entry_points.txt +0 -0
@@ -1,435 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import asyncio
|
4
|
-
import uuid
|
5
|
-
from contextlib import asynccontextmanager
|
6
|
-
from typing import TYPE_CHECKING, Any
|
7
|
-
|
8
|
-
import pytest
|
9
|
-
|
10
|
-
try:
|
11
|
-
from azure.core.exceptions import ResourceExistsError
|
12
|
-
from azure.storage.blob._shared.policies_async import ExponentialRetry
|
13
|
-
from azure.storage.blob.aio import BlobServiceClient
|
14
|
-
|
15
|
-
from planar.files.storage.azure_blob import AzureBlobStorage
|
16
|
-
|
17
|
-
azure_available = True
|
18
|
-
import_error = None
|
19
|
-
except ImportError as e:
|
20
|
-
import_error = e
|
21
|
-
azure_available = False
|
22
|
-
|
23
|
-
# Avoid evaluating runtime annotations when Azure SDK isn't installed
|
24
|
-
if TYPE_CHECKING or azure_available:
|
25
|
-
# Only imported for type checking; not at runtime
|
26
|
-
from azure.core.exceptions import ResourceExistsError
|
27
|
-
from azure.storage.blob._shared.policies_async import ExponentialRetry
|
28
|
-
from azure.storage.blob.aio import BlobServiceClient
|
29
|
-
|
30
|
-
from planar.files.storage.azure_blob import AzureBlobStorage # pragma: no cover
|
31
|
-
else:
|
32
|
-
AzureBlobStorage = Any # type: ignore
|
33
|
-
|
34
|
-
from planar.logging import get_logger
|
35
|
-
|
36
|
-
pytestmark = [
|
37
|
-
pytest.mark.skipif(
|
38
|
-
not azure_available,
|
39
|
-
reason=f"Azure blob not available: {import_error or 'unknown error'}",
|
40
|
-
),
|
41
|
-
pytest.mark.azure_blob,
|
42
|
-
]
|
43
|
-
|
44
|
-
|
45
|
-
logger = get_logger(__name__)
|
46
|
-
|
47
|
-
# --- Configuration for Azurite (Azure Storage Emulator) ---
|
48
|
-
|
49
|
-
AZURITE_ACCOUNT_NAME = "devstoreaccount1"
|
50
|
-
AZURITE_ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
51
|
-
AZURITE_ENDPOINT = "http://127.0.0.1:10000"
|
52
|
-
AZURITE_CONNECTION_STRING = (
|
53
|
-
f"DefaultEndpointsProtocol=http;AccountName={AZURITE_ACCOUNT_NAME};"
|
54
|
-
f"AccountKey={AZURITE_ACCOUNT_KEY};BlobEndpoint={AZURITE_ENDPOINT}/{AZURITE_ACCOUNT_NAME};"
|
55
|
-
)
|
56
|
-
|
57
|
-
# Generate a unique container name for each test run session
|
58
|
-
SESSION_CONTAINER_NAME = f"planar-test-{uuid.uuid4()}"
|
59
|
-
|
60
|
-
|
61
|
-
@pytest.fixture()
|
62
|
-
def azure_client():
|
63
|
-
"""Provides an Azure BlobServiceClient for direct interaction (e.g., container creation)."""
|
64
|
-
client = BlobServiceClient.from_connection_string(
|
65
|
-
AZURITE_CONNECTION_STRING,
|
66
|
-
connection_timeout=5,
|
67
|
-
read_timeout=5,
|
68
|
-
retry_policy=ExponentialRetry(
|
69
|
-
retry_total=1,
|
70
|
-
),
|
71
|
-
)
|
72
|
-
return client
|
73
|
-
|
74
|
-
|
75
|
-
@pytest.fixture(autouse=True)
|
76
|
-
async def ensure_azure_container(azure_client):
|
77
|
-
"""
|
78
|
-
Ensures the Azure container exists before tests run.
|
79
|
-
This runs automatically due to autouse=True.
|
80
|
-
"""
|
81
|
-
logger.warning(
|
82
|
-
"attempting to create container",
|
83
|
-
container_name=SESSION_CONTAINER_NAME,
|
84
|
-
storage_type="azurite",
|
85
|
-
)
|
86
|
-
|
87
|
-
try:
|
88
|
-
# Add timeout to container creation to fail fast if Azurite isn't running
|
89
|
-
async with azure_client:
|
90
|
-
container_client = azure_client.get_container_client(SESSION_CONTAINER_NAME)
|
91
|
-
await container_client.create_container(timeout=1)
|
92
|
-
logger.info("container created", container_name=SESSION_CONTAINER_NAME)
|
93
|
-
except ResourceExistsError:
|
94
|
-
logger.info("container already exists", container_name=SESSION_CONTAINER_NAME)
|
95
|
-
except asyncio.TimeoutError:
|
96
|
-
pytest.fail(
|
97
|
-
f"Timeout creating Azure container {SESSION_CONTAINER_NAME}. "
|
98
|
-
"Is Azurite running? Start with: docker run -p 127.0.0.1:10000:10000 -d mcr.microsoft.com/azure-storage/azurite azurite-blob"
|
99
|
-
)
|
100
|
-
except Exception as e:
|
101
|
-
pytest.fail(
|
102
|
-
f"Failed to create Azure container {SESSION_CONTAINER_NAME}: {e}. "
|
103
|
-
"Is Azurite running? Start with: docker run -p 127.0.0.1:10000:10000 -d mcr.microsoft.com/azure-storage/azurite azurite-blob"
|
104
|
-
)
|
105
|
-
|
106
|
-
yield # Tests run here
|
107
|
-
|
108
|
-
|
109
|
-
@pytest.fixture
|
110
|
-
async def azure_storage_connection_string():
|
111
|
-
"""Provides an AzureBlobStorage instance using connection string auth."""
|
112
|
-
storage_instance = AzureBlobStorage(
|
113
|
-
container_name=SESSION_CONTAINER_NAME,
|
114
|
-
connection_string=AZURITE_CONNECTION_STRING,
|
115
|
-
)
|
116
|
-
async with storage_instance as storage:
|
117
|
-
yield storage
|
118
|
-
|
119
|
-
|
120
|
-
@pytest.fixture
|
121
|
-
async def azure_storage_account_key():
|
122
|
-
"""Provides an AzureBlobStorage instance using account key auth."""
|
123
|
-
account_url = f"{AZURITE_ENDPOINT}/{AZURITE_ACCOUNT_NAME}"
|
124
|
-
storage_instance = AzureBlobStorage(
|
125
|
-
container_name=SESSION_CONTAINER_NAME,
|
126
|
-
account_url=account_url,
|
127
|
-
account_key=AZURITE_ACCOUNT_KEY,
|
128
|
-
)
|
129
|
-
async with storage_instance as storage:
|
130
|
-
yield storage
|
131
|
-
|
132
|
-
|
133
|
-
@asynccontextmanager
|
134
|
-
async def cleanup_azure_blob(storage: AzureBlobStorage, ref: str):
|
135
|
-
"""Context manager to ensure an Azure blob is deleted after use."""
|
136
|
-
try:
|
137
|
-
yield
|
138
|
-
finally:
|
139
|
-
try:
|
140
|
-
logger.debug("cleaning up blob", blob_ref=ref)
|
141
|
-
await storage.delete(ref)
|
142
|
-
except FileNotFoundError:
|
143
|
-
logger.debug("blob already deleted", blob_ref=ref)
|
144
|
-
except Exception as e:
|
145
|
-
logger.warning("blob cleanup failed", blob_ref=ref, error=str(e))
|
146
|
-
|
147
|
-
|
148
|
-
# --- Test Cases ---
|
149
|
-
async def test_put_get_bytes_connection_string(
|
150
|
-
azure_storage_connection_string: AzureBlobStorage,
|
151
|
-
):
|
152
|
-
"""Test storing and retrieving raw bytes using connection string auth."""
|
153
|
-
storage = azure_storage_connection_string
|
154
|
-
test_data = b"some binary data \x00\xff for azure blob"
|
155
|
-
mime_type = "application/octet-stream"
|
156
|
-
ref = None
|
157
|
-
|
158
|
-
try:
|
159
|
-
ref = await storage.put_bytes(test_data, mime_type=mime_type)
|
160
|
-
assert isinstance(ref, str)
|
161
|
-
|
162
|
-
# Validate ref is a UUID
|
163
|
-
try:
|
164
|
-
uuid.UUID(ref)
|
165
|
-
except ValueError:
|
166
|
-
pytest.fail(f"Returned ref '{ref}' is not a valid UUID string")
|
167
|
-
|
168
|
-
async with cleanup_azure_blob(storage, ref):
|
169
|
-
retrieved_data, retrieved_mime = await storage.get_bytes(ref)
|
170
|
-
|
171
|
-
assert retrieved_data == test_data
|
172
|
-
assert retrieved_mime == mime_type
|
173
|
-
|
174
|
-
# Check external URL (should be a SAS URL)
|
175
|
-
url = await storage.external_url(ref)
|
176
|
-
assert url is not None
|
177
|
-
assert SESSION_CONTAINER_NAME in url
|
178
|
-
assert ref in url
|
179
|
-
assert "sig=" in url # SAS signature
|
180
|
-
|
181
|
-
except Exception as e:
|
182
|
-
if ref:
|
183
|
-
await cleanup_azure_blob(storage, ref).__aexit__(None, None, None)
|
184
|
-
raise e
|
185
|
-
|
186
|
-
|
187
|
-
async def test_put_get_bytes_account_key(
|
188
|
-
azure_storage_account_key: AzureBlobStorage,
|
189
|
-
):
|
190
|
-
"""Test storing and retrieving raw bytes using account key auth."""
|
191
|
-
storage = azure_storage_account_key
|
192
|
-
test_data = b"azure blob test data with account key"
|
193
|
-
mime_type = "text/plain"
|
194
|
-
ref = None
|
195
|
-
|
196
|
-
try:
|
197
|
-
ref = await storage.put_bytes(test_data, mime_type=mime_type)
|
198
|
-
|
199
|
-
async with cleanup_azure_blob(storage, ref):
|
200
|
-
retrieved_data, retrieved_mime = await storage.get_bytes(ref)
|
201
|
-
|
202
|
-
assert retrieved_data == test_data
|
203
|
-
assert retrieved_mime == mime_type
|
204
|
-
|
205
|
-
# Check external URL
|
206
|
-
url = await storage.external_url(ref)
|
207
|
-
assert url is not None
|
208
|
-
assert ref in url
|
209
|
-
|
210
|
-
except Exception as e:
|
211
|
-
if ref:
|
212
|
-
await cleanup_azure_blob(storage, ref).__aexit__(None, None, None)
|
213
|
-
raise e
|
214
|
-
|
215
|
-
|
216
|
-
async def test_put_get_string(
|
217
|
-
azure_storage_connection_string: AzureBlobStorage,
|
218
|
-
):
|
219
|
-
"""Test storing and retrieving a string."""
|
220
|
-
storage = azure_storage_connection_string
|
221
|
-
test_string = "Hello, Azure Blob Storage! This is a test string with Unicode: éàçü."
|
222
|
-
mime_type = "text/plain"
|
223
|
-
encoding = "utf-8"
|
224
|
-
ref = None
|
225
|
-
|
226
|
-
try:
|
227
|
-
ref = await storage.put_string(
|
228
|
-
test_string, encoding=encoding, mime_type=mime_type
|
229
|
-
)
|
230
|
-
expected_mime_type = f"{mime_type}; charset={encoding}"
|
231
|
-
|
232
|
-
async with cleanup_azure_blob(storage, ref):
|
233
|
-
retrieved_string, retrieved_mime = await storage.get_string(
|
234
|
-
ref, encoding=encoding
|
235
|
-
)
|
236
|
-
|
237
|
-
assert retrieved_string == test_string
|
238
|
-
assert retrieved_mime == expected_mime_type
|
239
|
-
|
240
|
-
except Exception as e:
|
241
|
-
if ref:
|
242
|
-
await cleanup_azure_blob(storage, ref).__aexit__(None, None, None)
|
243
|
-
raise e
|
244
|
-
|
245
|
-
|
246
|
-
async def test_put_get_stream(
|
247
|
-
azure_storage_connection_string: AzureBlobStorage,
|
248
|
-
):
|
249
|
-
"""Test storing data from an async generator stream."""
|
250
|
-
storage = azure_storage_connection_string
|
251
|
-
test_chunks = [b"azure_chunk1 ", b"azure_chunk2 ", b"azure_chunk3"]
|
252
|
-
full_data = b"".join(test_chunks)
|
253
|
-
mime_type = "application/json"
|
254
|
-
ref = None
|
255
|
-
|
256
|
-
async def _test_stream():
|
257
|
-
for chunk in test_chunks:
|
258
|
-
yield chunk
|
259
|
-
await asyncio.sleep(0.01) # Simulate async work
|
260
|
-
|
261
|
-
try:
|
262
|
-
ref = await storage.put(_test_stream(), mime_type=mime_type)
|
263
|
-
|
264
|
-
async with cleanup_azure_blob(storage, ref):
|
265
|
-
stream, retrieved_mime = await storage.get(ref)
|
266
|
-
retrieved_data = b""
|
267
|
-
async for chunk in stream:
|
268
|
-
retrieved_data += chunk
|
269
|
-
|
270
|
-
assert retrieved_data == full_data
|
271
|
-
assert retrieved_mime == mime_type
|
272
|
-
|
273
|
-
except Exception as e:
|
274
|
-
if ref:
|
275
|
-
await cleanup_azure_blob(storage, ref).__aexit__(None, None, None)
|
276
|
-
raise e
|
277
|
-
|
278
|
-
|
279
|
-
async def test_put_no_mime_type(
|
280
|
-
azure_storage_connection_string: AzureBlobStorage,
|
281
|
-
):
|
282
|
-
"""Test storing data without providing a mime type."""
|
283
|
-
storage = azure_storage_connection_string
|
284
|
-
test_data = b"azure data without mime"
|
285
|
-
ref = None
|
286
|
-
|
287
|
-
try:
|
288
|
-
ref = await storage.put_bytes(test_data)
|
289
|
-
|
290
|
-
async with cleanup_azure_blob(storage, ref):
|
291
|
-
retrieved_data, retrieved_mime = await storage.get_bytes(ref)
|
292
|
-
|
293
|
-
assert retrieved_data == test_data
|
294
|
-
# Azure might not set a mime type if none provided
|
295
|
-
logger.debug(
|
296
|
-
"retrieved mime type", mime_type=retrieved_mime, provided=False
|
297
|
-
)
|
298
|
-
|
299
|
-
except Exception as e:
|
300
|
-
if ref:
|
301
|
-
await cleanup_azure_blob(storage, ref).__aexit__(None, None, None)
|
302
|
-
raise e
|
303
|
-
|
304
|
-
|
305
|
-
async def test_delete(
|
306
|
-
azure_storage_connection_string: AzureBlobStorage,
|
307
|
-
):
|
308
|
-
"""Test deleting stored data."""
|
309
|
-
storage = azure_storage_connection_string
|
310
|
-
ref = await storage.put_bytes(b"to be deleted from azure", mime_type="text/plain")
|
311
|
-
|
312
|
-
# Verify blob exists before delete
|
313
|
-
try:
|
314
|
-
_, _ = await storage.get(ref)
|
315
|
-
except FileNotFoundError:
|
316
|
-
pytest.fail(f"Blob {ref} should exist before deletion but was not found.")
|
317
|
-
|
318
|
-
# Delete the blob
|
319
|
-
await storage.delete(ref)
|
320
|
-
|
321
|
-
# Verify blob is gone after delete
|
322
|
-
with pytest.raises(FileNotFoundError):
|
323
|
-
await storage.get(ref)
|
324
|
-
|
325
|
-
# Deleting again should be idempotent (no error)
|
326
|
-
try:
|
327
|
-
await storage.delete(ref)
|
328
|
-
except Exception as e:
|
329
|
-
pytest.fail(f"Deleting already deleted ref raised an exception: {e}")
|
330
|
-
|
331
|
-
|
332
|
-
async def test_get_non_existent(
|
333
|
-
azure_storage_connection_string: AzureBlobStorage,
|
334
|
-
):
|
335
|
-
"""Test getting a reference that does not exist."""
|
336
|
-
storage = azure_storage_connection_string
|
337
|
-
non_existent_ref = str(uuid.uuid4())
|
338
|
-
|
339
|
-
with pytest.raises(FileNotFoundError):
|
340
|
-
await storage.get(non_existent_ref)
|
341
|
-
|
342
|
-
|
343
|
-
async def test_delete_non_existent(
|
344
|
-
azure_storage_connection_string: AzureBlobStorage,
|
345
|
-
):
|
346
|
-
"""Test deleting a reference that does not exist (should not raise error)."""
|
347
|
-
storage = azure_storage_connection_string
|
348
|
-
non_existent_ref = str(uuid.uuid4())
|
349
|
-
|
350
|
-
try:
|
351
|
-
await storage.delete(non_existent_ref)
|
352
|
-
except Exception as e:
|
353
|
-
pytest.fail(f"Deleting non-existent ref raised an exception: {e}")
|
354
|
-
|
355
|
-
|
356
|
-
async def test_external_url_connection_string(
|
357
|
-
azure_storage_connection_string: AzureBlobStorage,
|
358
|
-
):
|
359
|
-
"""Test that external_url returns a valid-looking SAS URL with connection string auth."""
|
360
|
-
storage = azure_storage_connection_string
|
361
|
-
ref = None
|
362
|
-
|
363
|
-
try:
|
364
|
-
ref = await storage.put_bytes(b"some data for url test")
|
365
|
-
|
366
|
-
async with cleanup_azure_blob(storage, ref):
|
367
|
-
url = await storage.external_url(ref)
|
368
|
-
assert url is not None
|
369
|
-
assert ref in url
|
370
|
-
assert SESSION_CONTAINER_NAME in url
|
371
|
-
# SAS URLs should have these query parameters
|
372
|
-
assert "sig=" in url # Signature
|
373
|
-
assert "se=" in url # Expiry time
|
374
|
-
|
375
|
-
except Exception as e:
|
376
|
-
if ref:
|
377
|
-
await cleanup_azure_blob(storage, ref).__aexit__(None, None, None)
|
378
|
-
raise e
|
379
|
-
|
380
|
-
|
381
|
-
def test_config_validation():
|
382
|
-
"""Test that the configuration validation works properly."""
|
383
|
-
from planar.files.storage.config import AzureBlobConfig
|
384
|
-
|
385
|
-
# These should all validate successfully
|
386
|
-
# Test connection string only (valid)
|
387
|
-
AzureBlobConfig(
|
388
|
-
backend="azure_blob",
|
389
|
-
container_name="test",
|
390
|
-
connection_string="DefaultEndpointsProtocol=https;AccountName=test;AccountKey=key;",
|
391
|
-
)
|
392
|
-
|
393
|
-
# Test account_url + account_key (valid)
|
394
|
-
AzureBlobConfig(
|
395
|
-
backend="azure_blob",
|
396
|
-
container_name="test",
|
397
|
-
account_url="https://test.blob.core.windows.net",
|
398
|
-
account_key="test-key",
|
399
|
-
)
|
400
|
-
|
401
|
-
# Test account_url + use_azure_ad (valid)
|
402
|
-
AzureBlobConfig(
|
403
|
-
backend="azure_blob",
|
404
|
-
container_name="test",
|
405
|
-
account_url="https://test.blob.core.windows.net",
|
406
|
-
use_azure_ad=True,
|
407
|
-
)
|
408
|
-
|
409
|
-
# Test invalid configs
|
410
|
-
with pytest.raises(ValueError, match="connection_string"):
|
411
|
-
# Connection string + other options
|
412
|
-
AzureBlobConfig(
|
413
|
-
backend="azure_blob",
|
414
|
-
container_name="test",
|
415
|
-
connection_string="test",
|
416
|
-
account_url="https://test.blob.core.windows.net",
|
417
|
-
)
|
418
|
-
|
419
|
-
with pytest.raises(ValueError, match="account_url must be provided"):
|
420
|
-
# No connection string and no account_url
|
421
|
-
AzureBlobConfig(
|
422
|
-
backend="azure_blob",
|
423
|
-
container_name="test",
|
424
|
-
use_azure_ad=True,
|
425
|
-
)
|
426
|
-
|
427
|
-
with pytest.raises(ValueError, match="exactly one credential method"):
|
428
|
-
# Both account_key and use_azure_ad
|
429
|
-
AzureBlobConfig(
|
430
|
-
backend="azure_blob",
|
431
|
-
container_name="test",
|
432
|
-
account_url="https://test.blob.core.windows.net",
|
433
|
-
account_key="key",
|
434
|
-
use_azure_ad=True,
|
435
|
-
)
|
@@ -1,162 +0,0 @@
|
|
1
|
-
import asyncio
|
2
|
-
import uuid
|
3
|
-
from pathlib import Path
|
4
|
-
|
5
|
-
import aiofiles.os
|
6
|
-
import pytest
|
7
|
-
|
8
|
-
from planar.files.storage.local_directory import LocalDirectoryStorage
|
9
|
-
|
10
|
-
|
11
|
-
@pytest.fixture
|
12
|
-
async def storage(tmp_path: Path) -> LocalDirectoryStorage:
|
13
|
-
"""Provides an instance of LocalDirectoryStorage using a temporary directory."""
|
14
|
-
storage_instance = LocalDirectoryStorage(tmp_path)
|
15
|
-
# Ensure subdirectories exist (though constructor should handle this)
|
16
|
-
await aiofiles.os.makedirs(storage_instance.blob_dir, exist_ok=True)
|
17
|
-
await aiofiles.os.makedirs(storage_instance.mime_dir, exist_ok=True)
|
18
|
-
return storage_instance
|
19
|
-
|
20
|
-
|
21
|
-
async def test_put_get_bytes(storage: LocalDirectoryStorage):
|
22
|
-
"""Test storing and retrieving raw bytes."""
|
23
|
-
test_data = b"some binary data \x00\xff"
|
24
|
-
mime_type = "application/octet-stream"
|
25
|
-
|
26
|
-
ref = await storage.put_bytes(test_data, mime_type=mime_type)
|
27
|
-
assert isinstance(ref, str)
|
28
|
-
try:
|
29
|
-
uuid.UUID(ref) # Check if ref is a valid UUID string
|
30
|
-
except ValueError:
|
31
|
-
pytest.fail(f"Returned ref '{ref}' is not a valid UUID string")
|
32
|
-
|
33
|
-
retrieved_data, retrieved_mime = await storage.get_bytes(ref)
|
34
|
-
|
35
|
-
assert retrieved_data == test_data
|
36
|
-
assert retrieved_mime == mime_type
|
37
|
-
|
38
|
-
# Check underlying files exist
|
39
|
-
blob_path = storage._get_path(ref, storage.BLOB_SUBDIR)
|
40
|
-
mime_path = storage._get_path(ref, storage.MIME_SUBDIR)
|
41
|
-
assert await aiofiles.os.path.exists(blob_path)
|
42
|
-
assert await aiofiles.os.path.exists(mime_path)
|
43
|
-
|
44
|
-
|
45
|
-
async def test_put_get_string(storage: LocalDirectoryStorage):
|
46
|
-
"""Test storing and retrieving a string."""
|
47
|
-
test_string = "Hello, world! This is a test string with Unicode: éàçü."
|
48
|
-
mime_type = "text/plain"
|
49
|
-
encoding = "utf-16"
|
50
|
-
|
51
|
-
# Store with explicit encoding and mime type
|
52
|
-
ref = await storage.put_string(test_string, encoding=encoding, mime_type=mime_type)
|
53
|
-
expected_mime_type = f"{mime_type}; charset={encoding}"
|
54
|
-
|
55
|
-
retrieved_string, retrieved_mime = await storage.get_string(ref, encoding=encoding)
|
56
|
-
|
57
|
-
assert retrieved_string == test_string
|
58
|
-
assert retrieved_mime == expected_mime_type
|
59
|
-
|
60
|
-
# Test default encoding (utf-8)
|
61
|
-
ref_utf8 = await storage.put_string(test_string, mime_type="text/html")
|
62
|
-
expected_mime_utf8 = "text/html; charset=utf-8"
|
63
|
-
retrieved_string_utf8, retrieved_mime_utf8 = await storage.get_string(ref_utf8)
|
64
|
-
assert retrieved_string_utf8 == test_string
|
65
|
-
assert retrieved_mime_utf8 == expected_mime_utf8
|
66
|
-
|
67
|
-
|
68
|
-
async def test_put_get_stream(storage: LocalDirectoryStorage):
|
69
|
-
"""Test storing data from an async generator stream."""
|
70
|
-
test_chunks = [b"chunk1 ", b"chunk2 ", b"chunk3"]
|
71
|
-
full_data = b"".join(test_chunks)
|
72
|
-
mime_type = "image/png"
|
73
|
-
|
74
|
-
async def _test_stream():
|
75
|
-
for chunk in test_chunks:
|
76
|
-
yield chunk
|
77
|
-
await asyncio.sleep(0.01) # Simulate async work
|
78
|
-
|
79
|
-
ref = await storage.put(_test_stream(), mime_type=mime_type)
|
80
|
-
|
81
|
-
stream, retrieved_mime = await storage.get(ref)
|
82
|
-
retrieved_data = b""
|
83
|
-
async for chunk in stream:
|
84
|
-
retrieved_data += chunk
|
85
|
-
|
86
|
-
assert retrieved_data == full_data
|
87
|
-
assert retrieved_mime == mime_type
|
88
|
-
|
89
|
-
|
90
|
-
async def test_put_no_mime_type(storage: LocalDirectoryStorage):
|
91
|
-
"""Test storing data without providing a mime type."""
|
92
|
-
test_data = b"data without mime"
|
93
|
-
|
94
|
-
ref = await storage.put_bytes(test_data)
|
95
|
-
retrieved_data, retrieved_mime = await storage.get_bytes(ref)
|
96
|
-
|
97
|
-
assert retrieved_data == test_data
|
98
|
-
assert retrieved_mime is None
|
99
|
-
|
100
|
-
# Check only blob file exists
|
101
|
-
blob_path = storage._get_path(ref, storage.BLOB_SUBDIR)
|
102
|
-
mime_path = storage._get_path(ref, storage.MIME_SUBDIR)
|
103
|
-
assert await aiofiles.os.path.exists(blob_path)
|
104
|
-
assert not await aiofiles.os.path.exists(mime_path)
|
105
|
-
|
106
|
-
|
107
|
-
async def test_delete(storage: LocalDirectoryStorage):
|
108
|
-
"""Test deleting stored data."""
|
109
|
-
ref = await storage.put_bytes(b"to be deleted", mime_type="text/plain")
|
110
|
-
|
111
|
-
blob_path = storage._get_path(ref, storage.BLOB_SUBDIR)
|
112
|
-
mime_path = storage._get_path(ref, storage.MIME_SUBDIR)
|
113
|
-
|
114
|
-
# Verify files exist before delete
|
115
|
-
assert await aiofiles.os.path.exists(blob_path)
|
116
|
-
assert await aiofiles.os.path.exists(mime_path)
|
117
|
-
|
118
|
-
await storage.delete(ref)
|
119
|
-
|
120
|
-
# Verify files are gone after delete
|
121
|
-
assert not await aiofiles.os.path.exists(blob_path)
|
122
|
-
assert not await aiofiles.os.path.exists(mime_path)
|
123
|
-
|
124
|
-
# Try getting deleted ref
|
125
|
-
with pytest.raises(FileNotFoundError):
|
126
|
-
await storage.get(ref)
|
127
|
-
|
128
|
-
|
129
|
-
async def test_get_non_existent(storage: LocalDirectoryStorage):
|
130
|
-
"""Test getting a reference that does not exist."""
|
131
|
-
non_existent_ref = str(uuid.uuid4())
|
132
|
-
with pytest.raises(FileNotFoundError):
|
133
|
-
await storage.get(non_existent_ref)
|
134
|
-
|
135
|
-
|
136
|
-
async def test_delete_non_existent(storage: LocalDirectoryStorage):
|
137
|
-
"""Test deleting a reference that does not exist (should not raise error)."""
|
138
|
-
non_existent_ref = str(uuid.uuid4())
|
139
|
-
try:
|
140
|
-
await storage.delete(non_existent_ref)
|
141
|
-
except Exception as e:
|
142
|
-
pytest.fail(f"Deleting non-existent ref raised an exception: {e}")
|
143
|
-
|
144
|
-
|
145
|
-
async def test_invalid_ref_format(storage: LocalDirectoryStorage):
|
146
|
-
"""Test operations with an invalid storage reference format."""
|
147
|
-
invalid_ref = "not-a-uuid"
|
148
|
-
with pytest.raises(ValueError):
|
149
|
-
await storage.get(invalid_ref)
|
150
|
-
|
151
|
-
with pytest.raises(ValueError):
|
152
|
-
await storage.delete(invalid_ref)
|
153
|
-
|
154
|
-
with pytest.raises(ValueError):
|
155
|
-
storage._get_path(invalid_ref, storage.BLOB_SUBDIR)
|
156
|
-
|
157
|
-
|
158
|
-
async def test_external_url(storage: LocalDirectoryStorage):
|
159
|
-
"""Test that external_url returns None for local storage."""
|
160
|
-
ref = await storage.put_bytes(b"some data")
|
161
|
-
url = await storage.external_url(ref)
|
162
|
-
assert url is None
|