kiarina-lib-google-cloud-storage 1.6.1__py3-none-any.whl → 1.6.3__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.
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from typing import Any
2
3
 
3
4
  from google.cloud import storage # type: ignore[import-untyped]
@@ -9,20 +10,81 @@ from .settings import settings_manager
9
10
  def get_blob(
10
11
  blob_name: str | None = None,
11
12
  *,
13
+ placeholders: dict[str, Any] | None = None,
12
14
  config_key: str | None = None,
13
15
  auth_config_key: str | None = None,
14
16
  **kwargs: Any,
15
17
  ) -> storage.Blob:
16
- settings = settings_manager.get_settings_by_key(config_key)
18
+ """
19
+ Get a Google Cloud Storage blob.
17
20
 
18
- if blob_name is None and settings.blob_name is None:
19
- raise ValueError("blob_name is not set in the settings and not provided")
21
+ Args:
22
+ blob_name: Full blob name (path). If provided, this takes precedence.
23
+ placeholders: Placeholders for blob_name_pattern formatting.
24
+ config_key: Configuration key for storage settings.
25
+ auth_config_key: Configuration key for authentication.
26
+ **kwargs: Additional arguments passed to get_bucket().
20
27
 
21
- if blob_name is None:
22
- blob_name = settings.blob_name
28
+ Returns:
29
+ Google Cloud Storage blob.
23
30
 
24
- if settings.blob_name_prefix:
25
- blob_name = f"{settings.blob_name_prefix}/{blob_name}"
31
+ Raises:
32
+ ValueError: If blob_name cannot be determined or pattern formatting fails.
33
+
34
+ Examples:
35
+ # Direct blob name
36
+ blob = get_blob(blob_name="data/file.json")
37
+
38
+ # Using pattern with placeholders
39
+ blob = get_blob(placeholders={"user_id": "123", "basename": "profile.json"})
40
+ # With pattern "users/{user_id}/{basename}" -> "users/123/profile.json"
41
+
42
+ # Using fixed pattern from settings (no placeholders)
43
+ blob = get_blob() # Uses settings.blob_name_pattern if it has no placeholders
44
+ """
45
+ settings = settings_manager.get_settings(config_key)
46
+
47
+ # Priority 1: Explicit blob_name
48
+ if blob_name is not None:
49
+ final_blob_name = blob_name
50
+
51
+ # Priority 2: Pattern with placeholders
52
+ elif placeholders is not None:
53
+ if settings.blob_name_pattern is None:
54
+ raise ValueError(
55
+ "placeholders provided but blob_name_pattern is not set in settings"
56
+ )
57
+
58
+ try:
59
+ final_blob_name = settings.blob_name_pattern.format(**placeholders)
60
+ except KeyError as e:
61
+ raise ValueError(
62
+ f"Missing placeholder {e} in blob_name_pattern: "
63
+ f"{settings.blob_name_pattern}. "
64
+ f"Available placeholders: {list(placeholders.keys())}"
65
+ ) from e
66
+
67
+ # Priority 3: Default pattern from settings
68
+ elif settings.blob_name_pattern is not None:
69
+ final_blob_name = settings.blob_name_pattern
70
+
71
+ else:
72
+ raise ValueError(
73
+ "blob_name is not provided, placeholders are not provided, "
74
+ "and blob_name_pattern is not set in settings"
75
+ )
76
+
77
+ # Safety check: Ensure no unresolved placeholders remain
78
+ if _has_placeholders(final_blob_name):
79
+ raise ValueError(
80
+ f"Unresolved placeholders found in blob name: {final_blob_name}. "
81
+ f"Please provide placeholders argument to resolve them."
82
+ )
26
83
 
27
84
  bucket = get_bucket(config_key, auth_config_key=auth_config_key, **kwargs)
28
- return bucket.blob(blob_name)
85
+ return bucket.blob(final_blob_name)
86
+
87
+
88
+ def _has_placeholders(pattern: str) -> bool:
89
+ """Check if a pattern contains placeholders like {key}."""
90
+ return bool(re.search(r"\{[^}]+\}", pattern))
@@ -12,10 +12,6 @@ def get_bucket(
12
12
  auth_config_key: str | None = None,
13
13
  **kwargs: Any,
14
14
  ) -> storage.Bucket:
15
- settings = settings_manager.get_settings_by_key(config_key)
16
-
17
- if settings.bucket_name is None:
18
- raise ValueError("bucket_name is not set in the settings")
19
-
15
+ settings = settings_manager.get_settings(config_key)
20
16
  client = get_storage_client(auth_config_key, **kwargs)
21
17
  return client.bucket(settings.bucket_name)
@@ -3,11 +3,17 @@ from pydantic_settings_manager import SettingsManager
3
3
 
4
4
 
5
5
  class GoogleCloudStorageSettings(BaseSettings):
6
- bucket_name: str | None = None
6
+ bucket_name: str
7
7
 
8
- blob_name_prefix: str | None = None
8
+ blob_name_pattern: str | None = None
9
+ """
10
+ Blob name pattern with placeholders.
9
11
 
10
- blob_name: str | None = None
12
+ Examples:
13
+ - "data.json" (fixed name)
14
+ - "files/{basename}" (single placeholder)
15
+ - "my-service/{tenant_id}/users/{user_id}/files/{basename}" (multiple placeholders)
16
+ """
11
17
 
12
18
 
13
19
  settings_manager = SettingsManager(GoogleCloudStorageSettings, multi=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiarina-lib-google-cloud-storage
3
- Version: 1.6.1
3
+ Version: 1.6.3
4
4
  Summary: Google Cloud Storage client library for kiarina namespace
5
5
  Project-URL: Homepage, https://github.com/kiarina/kiarina-python
6
6
  Project-URL: Repository, https://github.com/kiarina/kiarina-python
@@ -22,7 +22,7 @@ Classifier: Typing :: Typed
22
22
  Requires-Python: >=3.12
23
23
  Requires-Dist: google-cloud-storage>=2.19.0
24
24
  Requires-Dist: kiarina-lib-google-auth>=1.4.0
25
- Requires-Dist: pydantic-settings-manager>=2.1.0
25
+ Requires-Dist: pydantic-settings-manager>=2.3.0
26
26
  Requires-Dist: pydantic-settings>=2.10.1
27
27
  Description-Content-Type: text/markdown
28
28
 
@@ -32,7 +32,7 @@ A Python client library for Google Cloud Storage that separates infrastructure c
32
32
 
33
33
  ## Design Philosophy
34
34
 
35
- This library follows the principle of **separating infrastructure concerns from application logic**.
35
+ This library follows the principle of **separating infrastructure concerns from application logic**, with special emphasis on **security rule alignment** and **path structure management**.
36
36
 
37
37
  ### The Problem
38
38
 
@@ -58,17 +58,22 @@ blob = bucket.blob("v2/users/data.json") # Hard-coded path structure
58
58
  - Hard to support multiple environments (dev, staging, production)
59
59
  - Challenging to implement multi-tenancy
60
60
  - Credentials management is error-prone
61
+ - **Path structures are coupled with application code, making security rule changes difficult**
61
62
 
62
- ### The Solution
63
+ ### The Solution: Blob Name Patterns
63
64
 
64
- This library externalizes all infrastructure configuration:
65
+ This library externalizes all infrastructure configuration, including path structures:
65
66
 
66
67
  ```python
67
- # ✅ Application code only knows logical names
68
+ # ✅ Application code only provides variables
68
69
  from kiarina.lib.google.cloud_storage import get_blob
69
70
 
70
- # All infrastructure details are managed externally
71
- blob = get_blob(blob_name="user_data.json")
71
+ # Path structure is managed in configuration
72
+ blob = get_blob(placeholders={
73
+ "user_id": user_id,
74
+ "agent_id": agent_id,
75
+ "basename": file_name
76
+ })
72
77
  blob.upload_from_string(json.dumps(data))
73
78
  ```
74
79
 
@@ -78,6 +83,122 @@ blob.upload_from_string(json.dumps(data))
78
83
  - **Multi-tenant ready**: Different configurations for different tenants
79
84
  - **Secure**: Credentials managed through kiarina-lib-google-auth
80
85
  - **Maintainable**: Infrastructure changes don't require code changes
86
+ - **Security-aligned**: Path structures match GCS security rules, managed together
87
+
88
+ ### Why Blob Name Patterns Matter
89
+
90
+ **The Core Problem**: GCS security rules define path structures, and application code must align with them.
91
+
92
+ #### Without Blob Name Patterns (Tight Coupling)
93
+
94
+ ```python
95
+ # Application code constructs paths
96
+ blob_name = f"users/{user_id}/files/{file_name}"
97
+ blob = get_blob(blob_name=blob_name)
98
+ ```
99
+
100
+ **GCS Security Rules:**
101
+ ```javascript
102
+ rules_version = '2';
103
+ service firebase.storage {
104
+ match /b/{bucket}/o {
105
+ match /users/{user_id}/files/{basename} {
106
+ allow read, write: if request.auth.uid == user_id;
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ **What happens when security requirements change?**
113
+
114
+ New requirement: Add agent-level isolation for multi-tenancy.
115
+
116
+ **Updated Security Rules:**
117
+ ```javascript
118
+ match /my-service/{tenant_id}/users/{user_id}/files/{basename} {
119
+ allow read, write: if request.auth.uid == user_id
120
+ && request.auth.token.tenant_id == tenant_id;
121
+ }
122
+ ```
123
+
124
+ **Problem**: You must now update **every place** in your application code that constructs these paths:
125
+ ```python
126
+ # Must change all of these
127
+ blob_name = f"my-service/{tenant_id}/users/{user_id}/files/{file_name}" # Changed!
128
+ blob_name = f"my-service/{tenant_id}/users/{user_id}/thumbnails/{file_name}" # Changed!
129
+ blob_name = f"my-service/{tenant_id}/users/{user_id}/exports/{file_name}" # Changed!
130
+ # ... and many more
131
+ ```
132
+
133
+ #### With Blob Name Patterns (Loose Coupling)
134
+
135
+ **Configuration (Infrastructure Concern):**
136
+ ```yaml
137
+ # config/production.yaml
138
+ google_cloud_storage:
139
+ default:
140
+ bucket_name: "my-app-data"
141
+ blob_name_pattern: "my-service/{tenant_id}/users/{user_id}/files/{basename}"
142
+ ```
143
+
144
+ **GCS Security Rules (Infrastructure Concern):**
145
+ ```javascript
146
+ match /my-service/{tenant_id}/users/{user_id}/files/{basename} {
147
+ allow read, write: if request.auth.uid == user_id
148
+ && request.auth.token.tenant_id == tenant_id;
149
+ }
150
+ ```
151
+
152
+ **Application Code (Business Logic):**
153
+ ```python
154
+ # Application only provides variables - no path knowledge
155
+ blob = get_blob(placeholders={
156
+ "user_id": current_user.id,
157
+ "agent_id": current_agent.id,
158
+ "basename": uploaded_file.name
159
+ })
160
+ blob.upload_from_string(file_content)
161
+ ```
162
+
163
+ **When security rules change**: Only update the configuration file. Application code remains unchanged.
164
+
165
+ #### Real-World Example: Multi-Environment Security
166
+
167
+ Different environments often have different security requirements:
168
+
169
+ **Production** (strict isolation):
170
+ ```yaml
171
+ blob_name_pattern: "v2/production/{tenant_id}/{user_id}/{agent_id}/files/{basename}"
172
+ ```
173
+
174
+ **Staging** (relaxed for testing):
175
+ ```yaml
176
+ blob_name_pattern: "v2/staging/{user_id}/files/{basename}"
177
+ ```
178
+
179
+ **Development** (flat structure):
180
+ ```yaml
181
+ blob_name_pattern: "dev/{basename}"
182
+ ```
183
+
184
+ **Application code** (same for all environments):
185
+ ```python
186
+ blob = get_blob(placeholders={
187
+ "tenant_id": tenant.id,
188
+ "user_id": user.id,
189
+ "agent_id": agent.id,
190
+ "basename": file.name
191
+ })
192
+ ```
193
+
194
+ Missing placeholders are simply ignored if not present in the pattern.
195
+
196
+ ### Design Principles
197
+
198
+ 1. **Infrastructure defines "where"**: Path structures, bucket names, security rules
199
+ 2. **Application defines "what"**: Data content, business logic, variables
200
+ 3. **Configuration is the contract**: Placeholders define the interface between infrastructure and application
201
+ 4. **Security rules and path patterns are managed together**: Both are infrastructure concerns
81
202
 
82
203
  ## Features
83
204
 
@@ -106,14 +227,17 @@ from kiarina.lib.google.cloud_storage import get_blob, settings_manager
106
227
  settings_manager.user_config = {
107
228
  "default": {
108
229
  "bucket_name": "my-app-data",
109
- "blob_name_prefix": "production/v1"
230
+ "blob_name_pattern": "production/v1/{basename}"
110
231
  }
111
232
  }
112
233
 
113
234
  # Application code - clean and simple
114
- blob = get_blob(blob_name="user_data.json")
235
+ blob = get_blob(placeholders={"basename": "user_data.json"})
115
236
  # Actual path: gs://my-app-data/production/v1/user_data.json
116
237
 
238
+ # Or use direct blob name (full path)
239
+ blob = get_blob(blob_name="production/v1/user_data.json")
240
+
117
241
  # Use native google-cloud-storage API
118
242
  blob.upload_from_string("Hello, World!")
119
243
  content = blob.download_as_text()
@@ -244,14 +368,14 @@ def save_document(tenant_id: str, doc_id: str, content: bytes):
244
368
  def list_documents(tenant_id: str) -> list[str]:
245
369
  """List documents for any tenant"""
246
370
  from kiarina.lib.google.cloud_storage import get_bucket
247
-
371
+
248
372
  config_key = f"tenant_{tenant_id}"
249
373
  bucket = get_bucket(config_key=config_key, auth_config_key=config_key)
250
-
374
+
251
375
  # Get prefix from settings
252
- settings = settings_manager.get_settings_by_key(config_key)
376
+ settings = settings_manager.get_settings(config_key)
253
377
  prefix = f"{settings.blob_name_prefix}/documents/" if settings.blob_name_prefix else "documents/"
254
-
378
+
255
379
  blobs = bucket.list_blobs(prefix=prefix)
256
380
  return [blob.name for blob in blobs]
257
381
  ```
@@ -285,10 +409,10 @@ def mock_storage_config():
285
409
  def test_save_user_profile(mock_storage_config):
286
410
  """Test user profile saving"""
287
411
  from myapp.services import save_user_profile
288
-
412
+
289
413
  # Application code uses test configuration automatically
290
414
  save_user_profile("user123", {"name": "Alice"})
291
-
415
+
292
416
  # Verify using the same configuration
293
417
  from kiarina.lib.google.cloud_storage import get_blob
294
418
  blob = get_blob(blob_name="users/user123/profile.json")
@@ -304,20 +428,20 @@ from kiarina.lib.google.cloud_storage import settings_manager
304
428
 
305
429
  def debug_storage_config(config_key: str | None = None):
306
430
  """Show actual storage paths for debugging"""
307
- settings = settings_manager.get_settings_by_key(config_key)
308
-
431
+ settings = settings_manager.get_settings(config_key)
432
+
309
433
  print(f"Configuration: {config_key or 'default'}")
310
434
  print(f" Bucket: {settings.bucket_name}")
311
435
  print(f" Prefix: {settings.blob_name_prefix or '(none)'}")
312
436
  print(f" Auth: {settings.google_auth_config_key}")
313
-
437
+
314
438
  # Example paths
315
439
  example_blob = "users/123/profile.json"
316
440
  if settings.blob_name_prefix:
317
441
  full_path = f"{settings.blob_name_prefix}/{example_blob}"
318
442
  else:
319
443
  full_path = example_blob
320
-
444
+
321
445
  print(f" Example: gs://{settings.bucket_name}/{full_path}")
322
446
 
323
447
  # Usage
@@ -339,8 +463,7 @@ This library uses [pydantic-settings-manager](https://github.com/kiarina/pydanti
339
463
  | Field | Type | Required | Description |
340
464
  |-------|------|----------|-------------|
341
465
  | `bucket_name` | `str \| None` | Yes* | Google Cloud Storage bucket name |
342
- | `blob_name_prefix` | `str \| None` | No | Prefix for blob names (e.g., "production/v1") |
343
- | `blob_name` | `str \| None` | No | Default blob name (rarely used) |
466
+ | `blob_name_pattern` | `str \| None` | No | Blob name pattern with placeholders (e.g., "users/{user_id}/files/{basename}") |
344
467
 
345
468
  *Required when using `get_bucket()` or `get_blob()`
346
469
 
@@ -501,6 +624,7 @@ Get a Google Cloud Storage blob.
501
624
  def get_blob(
502
625
  blob_name: str | None = None,
503
626
  *,
627
+ placeholders: dict[str, Any] | None = None,
504
628
  config_key: str | None = None,
505
629
  auth_config_key: str | None = None,
506
630
  **kwargs: Any
@@ -508,7 +632,8 @@ def get_blob(
508
632
  ```
509
633
 
510
634
  **Parameters:**
511
- - `blob_name`: Blob name (default: None uses settings.blob_name)
635
+ - `blob_name`: Full blob name (path). If provided, this takes precedence.
636
+ - `placeholders`: Placeholders for blob_name_pattern formatting.
512
637
  - `config_key`: Configuration key for storage settings (default: None uses active key)
513
638
  - `auth_config_key`: Configuration key for authentication (default: None uses active key)
514
639
  - `**kwargs`: Additional arguments passed to `get_bucket()`
@@ -517,21 +642,41 @@ def get_blob(
517
642
  - `storage.Blob`: Google Cloud Storage blob
518
643
 
519
644
  **Raises:**
520
- - `ValueError`: If `blob_name` is not provided and not set in settings
645
+ - `ValueError`: If blob_name cannot be determined or pattern formatting fails
646
+
647
+ **Priority:**
648
+ 1. Explicit `blob_name` parameter (full path)
649
+ 2. `blob_name_pattern` with `placeholders`
650
+ 3. `blob_name_pattern` without placeholders (fixed name)
521
651
 
522
652
  **Example:**
523
653
  ```python
524
- # Basic usage
525
- blob = get_blob(blob_name="data.json")
526
-
527
- # With blob_name_prefix in settings
528
- # If blob_name_prefix="production/v1" and blob_name="data.json"
529
- # Actual blob name will be "production/v1/data.json"
530
- blob = get_blob(blob_name="data.json")
654
+ # Direct blob name (full path)
655
+ blob = get_blob(blob_name="production/v1/data.json")
656
+
657
+ # Using pattern with placeholders
658
+ # If blob_name_pattern="users/{user_id}/files/{basename}"
659
+ blob = get_blob(placeholders={"user_id": "123", "basename": "profile.json"})
660
+ # Actual: gs://bucket/users/123/files/profile.json
661
+
662
+ # Using fixed pattern from settings
663
+ # If blob_name_pattern="data/fixed.json"
664
+ blob = get_blob()
665
+ # Actual: gs://bucket/data/fixed.json
666
+
667
+ # Complex pattern
668
+ # If blob_name_pattern="my-service/{tenant_id}/users/{user_id}/files/{basename}"
669
+ blob = get_blob(placeholders={
670
+ "tenant_id": "tenant123",
671
+ "user_id": "user123",
672
+ "agent_id": "agent456",
673
+ "basename": "document.pdf"
674
+ })
675
+ # Actual: gs://bucket/my-service/tenant123/users/user123/files/document.pdf
531
676
 
532
677
  # With custom configurations
533
678
  blob = get_blob(
534
- blob_name="data.json",
679
+ placeholders={"basename": "data.json"},
535
680
  config_key="production",
536
681
  auth_config_key="prod_auth"
537
682
  )
@@ -556,7 +701,7 @@ settings_manager: SettingsManager[GoogleCloudStorageSettings]
556
701
  - `active_key`: Get/set active configuration key
557
702
 
558
703
  **Methods:**
559
- - `get_settings_by_key(key: str)`: Get settings by specific key
704
+ - `get_settings(key: str)`: Get settings by specific key
560
705
  - `clear()`: Clear cached settings
561
706
 
562
707
  ## Common Operations
@@ -0,0 +1,9 @@
1
+ kiarina/lib/google/cloud_storage/__init__.py,sha256=q2AdOOOHUj_D-eot8_1-atrIaCKsCg0YatUOdq3kVVE,1184
2
+ kiarina/lib/google/cloud_storage/_get_blob.py,sha256=j74xGy9ZlAFs2EwywQBVACprRcGOU63wd9FKWlE39dk,3065
3
+ kiarina/lib/google/cloud_storage/_get_bucket.py,sha256=1fBIuohwQzpsxaANpVTExQly3smwaYD2wtnDe7yg6Uo,484
4
+ kiarina/lib/google/cloud_storage/_get_storage_client.py,sha256=lmBR2I9_vcyb0PfsiSz0BPFYBwQ6zVci75cmk9GUN5U,359
5
+ kiarina/lib/google/cloud_storage/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ kiarina/lib/google/cloud_storage/settings.py,sha256=vKYeiVRL0qOPCJEbwJfdEUPU8KwpgMLLBzJ3eGpaqOI,536
7
+ kiarina_lib_google_cloud_storage-1.6.3.dist-info/METADATA,sha256=PByhsHVLVhbX9IwQdDhSqSo48ThFkC5H-T7rB2gA6JM,27553
8
+ kiarina_lib_google_cloud_storage-1.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ kiarina_lib_google_cloud_storage-1.6.3.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- kiarina/lib/google/cloud_storage/__init__.py,sha256=q2AdOOOHUj_D-eot8_1-atrIaCKsCg0YatUOdq3kVVE,1184
2
- kiarina/lib/google/cloud_storage/_get_blob.py,sha256=ymvAwPZmPYC407jmLgc0uCOoo3qGh8B2zssAVkLHX_I,817
3
- kiarina/lib/google/cloud_storage/_get_bucket.py,sha256=SaGIQqRb9UOJaosGf2pXduAEYscE_T21AHCNTaZtRGo,597
4
- kiarina/lib/google/cloud_storage/_get_storage_client.py,sha256=lmBR2I9_vcyb0PfsiSz0BPFYBwQ6zVci75cmk9GUN5U,359
5
- kiarina/lib/google/cloud_storage/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- kiarina/lib/google/cloud_storage/settings.py,sha256=q47sa7CjegzXT2jhIDFbAc1huESQzZ3N1tlcmtKnLGU,334
7
- kiarina_lib_google_cloud_storage-1.6.1.dist-info/METADATA,sha256=Nq6mP13cIxD4ETZRGSuWHXWy5sX7ZRbwF7IMAZ6ymLc,22959
8
- kiarina_lib_google_cloud_storage-1.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- kiarina_lib_google_cloud_storage-1.6.1.dist-info/RECORD,,