kiarina-lib-google-cloud-storage 1.6.1__tar.gz → 1.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/CHANGELOG.md +11 -0
  2. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/PKG-INFO +165 -21
  3. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/README.md +164 -20
  4. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/pyproject.toml +1 -1
  5. kiarina_lib_google_cloud_storage-1.6.2/src/kiarina/lib/google/cloud_storage/_get_blob.py +78 -0
  6. kiarina_lib_google_cloud_storage-1.6.2/src/kiarina/lib/google/cloud_storage/settings.py +19 -0
  7. kiarina_lib_google_cloud_storage-1.6.2/tests/test_get_blob.py +224 -0
  8. kiarina_lib_google_cloud_storage-1.6.1/src/kiarina/lib/google/cloud_storage/_get_blob.py +0 -28
  9. kiarina_lib_google_cloud_storage-1.6.1/src/kiarina/lib/google/cloud_storage/settings.py +0 -13
  10. kiarina_lib_google_cloud_storage-1.6.1/tests/test_get_blob.py +0 -175
  11. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/.gitignore +0 -0
  12. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/.vscode/settings.json +0 -0
  13. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/src/kiarina/lib/google/cloud_storage/__init__.py +0 -0
  14. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/src/kiarina/lib/google/cloud_storage/_get_bucket.py +0 -0
  15. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/src/kiarina/lib/google/cloud_storage/_get_storage_client.py +0 -0
  16. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/src/kiarina/lib/google/cloud_storage/py.typed +0 -0
  17. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/tests/__init__.py +0 -0
  18. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/tests/conftest.py +0 -0
  19. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/tests/test_get_bucket.py +0 -0
  20. {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/tests/test_get_storage_client.py +0 -0
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.6.2] - 2025-10-10
11
+
12
+ ### Changed
13
+ - **Improved blob name handling**: Replaced `blob_name_prefix` and `blob_name` with `blob_name_pattern`
14
+ - `blob_name_pattern` supports both fixed names and template patterns with placeholders
15
+ - `get_blob()` now accepts `placeholders` parameter for pattern formatting
16
+ - `blob_name` parameter in `get_blob()` now always represents the full blob path
17
+ - Pattern examples: `"data.json"` (fixed), `"files/{basename}"` (single placeholder), `"web/{user_id}/{agent_id}/files/{basename}"` (multiple placeholders)
18
+ - Priority: explicit `blob_name` → `blob_name_pattern` with `placeholders` → `blob_name_pattern` without placeholders
19
+ - Enhanced error messages for missing placeholders
20
+
10
21
  ## [1.6.1] - 2025-10-10
11
22
 
12
23
  ### Changed
@@ -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.2
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
@@ -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 /web/{user_id}/{agent_id}/files/{basename} {
119
+ allow read, write: if request.auth.uid == user_id
120
+ && request.auth.token.agent_id == agent_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"web/{user_id}/{agent_id}/files/{file_name}" # Changed!
128
+ blob_name = f"web/{user_id}/{agent_id}/thumbnails/{file_name}" # Changed!
129
+ blob_name = f"web/{user_id}/{agent_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: "web/{user_id}/{agent_id}/files/{basename}"
142
+ ```
143
+
144
+ **GCS Security Rules (Infrastructure Concern):**
145
+ ```javascript
146
+ match /web/{user_id}/{agent_id}/files/{basename} {
147
+ allow read, write: if request.auth.uid == user_id
148
+ && request.auth.token.agent_id == agent_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()
@@ -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,40 @@ 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="web/{user_id}/{agent_id}/files/{basename}"
669
+ blob = get_blob(placeholders={
670
+ "user_id": "user123",
671
+ "agent_id": "agent456",
672
+ "basename": "document.pdf"
673
+ })
674
+ # Actual: gs://bucket/web/user123/agent456/files/document.pdf
531
675
 
532
676
  # With custom configurations
533
677
  blob = get_blob(
534
- blob_name="data.json",
678
+ placeholders={"basename": "data.json"},
535
679
  config_key="production",
536
680
  auth_config_key="prod_auth"
537
681
  )
@@ -4,7 +4,7 @@ A Python client library for Google Cloud Storage that separates infrastructure c
4
4
 
5
5
  ## Design Philosophy
6
6
 
7
- This library follows the principle of **separating infrastructure concerns from application logic**.
7
+ This library follows the principle of **separating infrastructure concerns from application logic**, with special emphasis on **security rule alignment** and **path structure management**.
8
8
 
9
9
  ### The Problem
10
10
 
@@ -30,17 +30,22 @@ blob = bucket.blob("v2/users/data.json") # Hard-coded path structure
30
30
  - Hard to support multiple environments (dev, staging, production)
31
31
  - Challenging to implement multi-tenancy
32
32
  - Credentials management is error-prone
33
+ - **Path structures are coupled with application code, making security rule changes difficult**
33
34
 
34
- ### The Solution
35
+ ### The Solution: Blob Name Patterns
35
36
 
36
- This library externalizes all infrastructure configuration:
37
+ This library externalizes all infrastructure configuration, including path structures:
37
38
 
38
39
  ```python
39
- # ✅ Application code only knows logical names
40
+ # ✅ Application code only provides variables
40
41
  from kiarina.lib.google.cloud_storage import get_blob
41
42
 
42
- # All infrastructure details are managed externally
43
- blob = get_blob(blob_name="user_data.json")
43
+ # Path structure is managed in configuration
44
+ blob = get_blob(placeholders={
45
+ "user_id": user_id,
46
+ "agent_id": agent_id,
47
+ "basename": file_name
48
+ })
44
49
  blob.upload_from_string(json.dumps(data))
45
50
  ```
46
51
 
@@ -50,6 +55,122 @@ blob.upload_from_string(json.dumps(data))
50
55
  - **Multi-tenant ready**: Different configurations for different tenants
51
56
  - **Secure**: Credentials managed through kiarina-lib-google-auth
52
57
  - **Maintainable**: Infrastructure changes don't require code changes
58
+ - **Security-aligned**: Path structures match GCS security rules, managed together
59
+
60
+ ### Why Blob Name Patterns Matter
61
+
62
+ **The Core Problem**: GCS security rules define path structures, and application code must align with them.
63
+
64
+ #### Without Blob Name Patterns (Tight Coupling)
65
+
66
+ ```python
67
+ # Application code constructs paths
68
+ blob_name = f"users/{user_id}/files/{file_name}"
69
+ blob = get_blob(blob_name=blob_name)
70
+ ```
71
+
72
+ **GCS Security Rules:**
73
+ ```javascript
74
+ rules_version = '2';
75
+ service firebase.storage {
76
+ match /b/{bucket}/o {
77
+ match /users/{user_id}/files/{basename} {
78
+ allow read, write: if request.auth.uid == user_id;
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ **What happens when security requirements change?**
85
+
86
+ New requirement: Add agent-level isolation for multi-tenancy.
87
+
88
+ **Updated Security Rules:**
89
+ ```javascript
90
+ match /web/{user_id}/{agent_id}/files/{basename} {
91
+ allow read, write: if request.auth.uid == user_id
92
+ && request.auth.token.agent_id == agent_id;
93
+ }
94
+ ```
95
+
96
+ **Problem**: You must now update **every place** in your application code that constructs these paths:
97
+ ```python
98
+ # Must change all of these
99
+ blob_name = f"web/{user_id}/{agent_id}/files/{file_name}" # Changed!
100
+ blob_name = f"web/{user_id}/{agent_id}/thumbnails/{file_name}" # Changed!
101
+ blob_name = f"web/{user_id}/{agent_id}/exports/{file_name}" # Changed!
102
+ # ... and many more
103
+ ```
104
+
105
+ #### With Blob Name Patterns (Loose Coupling)
106
+
107
+ **Configuration (Infrastructure Concern):**
108
+ ```yaml
109
+ # config/production.yaml
110
+ google_cloud_storage:
111
+ default:
112
+ bucket_name: "my-app-data"
113
+ blob_name_pattern: "web/{user_id}/{agent_id}/files/{basename}"
114
+ ```
115
+
116
+ **GCS Security Rules (Infrastructure Concern):**
117
+ ```javascript
118
+ match /web/{user_id}/{agent_id}/files/{basename} {
119
+ allow read, write: if request.auth.uid == user_id
120
+ && request.auth.token.agent_id == agent_id;
121
+ }
122
+ ```
123
+
124
+ **Application Code (Business Logic):**
125
+ ```python
126
+ # Application only provides variables - no path knowledge
127
+ blob = get_blob(placeholders={
128
+ "user_id": current_user.id,
129
+ "agent_id": current_agent.id,
130
+ "basename": uploaded_file.name
131
+ })
132
+ blob.upload_from_string(file_content)
133
+ ```
134
+
135
+ **When security rules change**: Only update the configuration file. Application code remains unchanged.
136
+
137
+ #### Real-World Example: Multi-Environment Security
138
+
139
+ Different environments often have different security requirements:
140
+
141
+ **Production** (strict isolation):
142
+ ```yaml
143
+ blob_name_pattern: "v2/production/{tenant_id}/{user_id}/{agent_id}/files/{basename}"
144
+ ```
145
+
146
+ **Staging** (relaxed for testing):
147
+ ```yaml
148
+ blob_name_pattern: "v2/staging/{user_id}/files/{basename}"
149
+ ```
150
+
151
+ **Development** (flat structure):
152
+ ```yaml
153
+ blob_name_pattern: "dev/{basename}"
154
+ ```
155
+
156
+ **Application code** (same for all environments):
157
+ ```python
158
+ blob = get_blob(placeholders={
159
+ "tenant_id": tenant.id,
160
+ "user_id": user.id,
161
+ "agent_id": agent.id,
162
+ "basename": file.name
163
+ })
164
+ ```
165
+
166
+ Missing placeholders are simply ignored if not present in the pattern.
167
+
168
+ ### Design Principles
169
+
170
+ 1. **Infrastructure defines "where"**: Path structures, bucket names, security rules
171
+ 2. **Application defines "what"**: Data content, business logic, variables
172
+ 3. **Configuration is the contract**: Placeholders define the interface between infrastructure and application
173
+ 4. **Security rules and path patterns are managed together**: Both are infrastructure concerns
53
174
 
54
175
  ## Features
55
176
 
@@ -78,14 +199,17 @@ from kiarina.lib.google.cloud_storage import get_blob, settings_manager
78
199
  settings_manager.user_config = {
79
200
  "default": {
80
201
  "bucket_name": "my-app-data",
81
- "blob_name_prefix": "production/v1"
202
+ "blob_name_pattern": "production/v1/{basename}"
82
203
  }
83
204
  }
84
205
 
85
206
  # Application code - clean and simple
86
- blob = get_blob(blob_name="user_data.json")
207
+ blob = get_blob(placeholders={"basename": "user_data.json"})
87
208
  # Actual path: gs://my-app-data/production/v1/user_data.json
88
209
 
210
+ # Or use direct blob name (full path)
211
+ blob = get_blob(blob_name="production/v1/user_data.json")
212
+
89
213
  # Use native google-cloud-storage API
90
214
  blob.upload_from_string("Hello, World!")
91
215
  content = blob.download_as_text()
@@ -311,8 +435,7 @@ This library uses [pydantic-settings-manager](https://github.com/kiarina/pydanti
311
435
  | Field | Type | Required | Description |
312
436
  |-------|------|----------|-------------|
313
437
  | `bucket_name` | `str \| None` | Yes* | Google Cloud Storage bucket name |
314
- | `blob_name_prefix` | `str \| None` | No | Prefix for blob names (e.g., "production/v1") |
315
- | `blob_name` | `str \| None` | No | Default blob name (rarely used) |
438
+ | `blob_name_pattern` | `str \| None` | No | Blob name pattern with placeholders (e.g., "users/{user_id}/files/{basename}") |
316
439
 
317
440
  *Required when using `get_bucket()` or `get_blob()`
318
441
 
@@ -473,6 +596,7 @@ Get a Google Cloud Storage blob.
473
596
  def get_blob(
474
597
  blob_name: str | None = None,
475
598
  *,
599
+ placeholders: dict[str, Any] | None = None,
476
600
  config_key: str | None = None,
477
601
  auth_config_key: str | None = None,
478
602
  **kwargs: Any
@@ -480,7 +604,8 @@ def get_blob(
480
604
  ```
481
605
 
482
606
  **Parameters:**
483
- - `blob_name`: Blob name (default: None uses settings.blob_name)
607
+ - `blob_name`: Full blob name (path). If provided, this takes precedence.
608
+ - `placeholders`: Placeholders for blob_name_pattern formatting.
484
609
  - `config_key`: Configuration key for storage settings (default: None uses active key)
485
610
  - `auth_config_key`: Configuration key for authentication (default: None uses active key)
486
611
  - `**kwargs`: Additional arguments passed to `get_bucket()`
@@ -489,21 +614,40 @@ def get_blob(
489
614
  - `storage.Blob`: Google Cloud Storage blob
490
615
 
491
616
  **Raises:**
492
- - `ValueError`: If `blob_name` is not provided and not set in settings
617
+ - `ValueError`: If blob_name cannot be determined or pattern formatting fails
618
+
619
+ **Priority:**
620
+ 1. Explicit `blob_name` parameter (full path)
621
+ 2. `blob_name_pattern` with `placeholders`
622
+ 3. `blob_name_pattern` without placeholders (fixed name)
493
623
 
494
624
  **Example:**
495
625
  ```python
496
- # Basic usage
497
- blob = get_blob(blob_name="data.json")
498
-
499
- # With blob_name_prefix in settings
500
- # If blob_name_prefix="production/v1" and blob_name="data.json"
501
- # Actual blob name will be "production/v1/data.json"
502
- blob = get_blob(blob_name="data.json")
626
+ # Direct blob name (full path)
627
+ blob = get_blob(blob_name="production/v1/data.json")
628
+
629
+ # Using pattern with placeholders
630
+ # If blob_name_pattern="users/{user_id}/files/{basename}"
631
+ blob = get_blob(placeholders={"user_id": "123", "basename": "profile.json"})
632
+ # Actual: gs://bucket/users/123/files/profile.json
633
+
634
+ # Using fixed pattern from settings
635
+ # If blob_name_pattern="data/fixed.json"
636
+ blob = get_blob()
637
+ # Actual: gs://bucket/data/fixed.json
638
+
639
+ # Complex pattern
640
+ # If blob_name_pattern="web/{user_id}/{agent_id}/files/{basename}"
641
+ blob = get_blob(placeholders={
642
+ "user_id": "user123",
643
+ "agent_id": "agent456",
644
+ "basename": "document.pdf"
645
+ })
646
+ # Actual: gs://bucket/web/user123/agent456/files/document.pdf
503
647
 
504
648
  # With custom configurations
505
649
  blob = get_blob(
506
- blob_name="data.json",
650
+ placeholders={"basename": "data.json"},
507
651
  config_key="production",
508
652
  auth_config_key="prod_auth"
509
653
  )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiarina-lib-google-cloud-storage"
3
- version = "1.6.1"
3
+ version = "1.6.2"
4
4
  description = "Google Cloud Storage client library for kiarina namespace"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,78 @@
1
+ from typing import Any
2
+
3
+ from google.cloud import storage # type: ignore[import-untyped]
4
+
5
+ from ._get_bucket import get_bucket
6
+ from .settings import settings_manager
7
+
8
+
9
+ def get_blob(
10
+ blob_name: str | None = None,
11
+ *,
12
+ placeholders: dict[str, Any] | None = None,
13
+ config_key: str | None = None,
14
+ auth_config_key: str | None = None,
15
+ **kwargs: Any,
16
+ ) -> storage.Blob:
17
+ """
18
+ Get a Google Cloud Storage blob.
19
+
20
+ Args:
21
+ blob_name: Full blob name (path). If provided, this takes precedence.
22
+ placeholders: Placeholders for blob_name_pattern formatting.
23
+ config_key: Configuration key for storage settings.
24
+ auth_config_key: Configuration key for authentication.
25
+ **kwargs: Additional arguments passed to get_bucket().
26
+
27
+ Returns:
28
+ Google Cloud Storage blob.
29
+
30
+ Raises:
31
+ ValueError: If blob_name cannot be determined or pattern formatting fails.
32
+
33
+ Examples:
34
+ # Direct blob name
35
+ blob = get_blob(blob_name="data/file.json")
36
+
37
+ # Using pattern with placeholders
38
+ blob = get_blob(placeholders={"user_id": "123", "basename": "profile.json"})
39
+ # With pattern "users/{user_id}/{basename}" -> "users/123/profile.json"
40
+
41
+ # Using default pattern from settings
42
+ blob = get_blob() # Uses settings.blob_name_pattern without placeholders
43
+ """
44
+ settings = settings_manager.get_settings_by_key(config_key)
45
+
46
+ # Priority 1: Explicit blob_name
47
+ if blob_name is not None:
48
+ final_blob_name = blob_name
49
+
50
+ # Priority 2: Pattern with placeholders
51
+ elif placeholders is not None:
52
+ if settings.blob_name_pattern is None:
53
+ raise ValueError(
54
+ "placeholders provided but blob_name_pattern is not set in settings"
55
+ )
56
+
57
+ try:
58
+ final_blob_name = settings.blob_name_pattern.format(**placeholders)
59
+ except KeyError as e:
60
+ raise ValueError(
61
+ f"Missing placeholder {e} in blob_name_pattern: "
62
+ f"{settings.blob_name_pattern}. "
63
+ f"Available placeholders: {list(placeholders.keys())}"
64
+ ) from e
65
+
66
+ # Priority 3: Default pattern from settings
67
+ elif settings.blob_name_pattern is not None:
68
+ # Pattern without placeholders (fixed name)
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
+ bucket = get_bucket(config_key, auth_config_key=auth_config_key, **kwargs)
78
+ return bucket.blob(final_blob_name)
@@ -0,0 +1,19 @@
1
+ from pydantic_settings import BaseSettings
2
+ from pydantic_settings_manager import SettingsManager
3
+
4
+
5
+ class GoogleCloudStorageSettings(BaseSettings):
6
+ bucket_name: str | None = None
7
+
8
+ blob_name_pattern: str | None = None
9
+ """
10
+ Blob name pattern with placeholders.
11
+
12
+ Examples:
13
+ - "data.json" (fixed name)
14
+ - "files/{basename}" (single placeholder)
15
+ - "web/{user_id}/{agent_id}/files/{basename}" (multiple placeholders)
16
+ """
17
+
18
+
19
+ settings_manager = SettingsManager(GoogleCloudStorageSettings, multi=True)
@@ -0,0 +1,224 @@
1
+ from unittest.mock import MagicMock, patch
2
+
3
+ import pytest
4
+
5
+ from kiarina.lib.google.cloud_storage import get_blob, settings_manager
6
+
7
+
8
+ def test_get_blob_with_blob_name():
9
+ """Test get_blob with explicit blob_name parameter."""
10
+ settings_manager.user_config = {
11
+ "default": {
12
+ "bucket_name": "test-bucket",
13
+ }
14
+ }
15
+
16
+ mock_bucket = MagicMock()
17
+ mock_blob = MagicMock()
18
+ mock_blob.name = "data/file.json"
19
+ mock_bucket.blob.return_value = mock_blob
20
+
21
+ with patch(
22
+ "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
23
+ return_value=mock_bucket,
24
+ ):
25
+ blob = get_blob(blob_name="data/file.json")
26
+ assert blob.name == "data/file.json"
27
+ mock_bucket.blob.assert_called_once_with("data/file.json")
28
+
29
+
30
+ def test_get_blob_with_pattern_and_placeholders():
31
+ """Test get_blob with blob_name_pattern and placeholders."""
32
+ settings_manager.user_config = {
33
+ "default": {
34
+ "bucket_name": "test-bucket",
35
+ "blob_name_pattern": "users/{user_id}/files/{basename}",
36
+ }
37
+ }
38
+
39
+ mock_bucket = MagicMock()
40
+ mock_blob = MagicMock()
41
+ mock_blob.name = "users/123/files/profile.json"
42
+ mock_bucket.blob.return_value = mock_blob
43
+
44
+ with patch(
45
+ "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
46
+ return_value=mock_bucket,
47
+ ):
48
+ blob = get_blob(placeholders={"user_id": "123", "basename": "profile.json"})
49
+ assert blob.name == "users/123/files/profile.json"
50
+ mock_bucket.blob.assert_called_once_with("users/123/files/profile.json")
51
+
52
+
53
+ def test_get_blob_with_fixed_pattern():
54
+ """Test get_blob with fixed blob_name_pattern (no placeholders)."""
55
+ settings_manager.user_config = {
56
+ "default": {
57
+ "bucket_name": "test-bucket",
58
+ "blob_name_pattern": "data/fixed.json",
59
+ }
60
+ }
61
+
62
+ mock_bucket = MagicMock()
63
+ mock_blob = MagicMock()
64
+ mock_blob.name = "data/fixed.json"
65
+ mock_bucket.blob.return_value = mock_blob
66
+
67
+ with patch(
68
+ "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
69
+ return_value=mock_bucket,
70
+ ):
71
+ blob = get_blob()
72
+ assert blob.name == "data/fixed.json"
73
+ mock_bucket.blob.assert_called_once_with("data/fixed.json")
74
+
75
+
76
+ def test_get_blob_priority_blob_name_over_placeholders():
77
+ """Test that blob_name takes precedence over placeholders."""
78
+ settings_manager.user_config = {
79
+ "default": {
80
+ "bucket_name": "test-bucket",
81
+ "blob_name_pattern": "users/{user_id}/files/{basename}",
82
+ }
83
+ }
84
+
85
+ mock_bucket = MagicMock()
86
+ mock_blob = MagicMock()
87
+ mock_blob.name = "direct/path.json"
88
+ mock_bucket.blob.return_value = mock_blob
89
+
90
+ with patch(
91
+ "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
92
+ return_value=mock_bucket,
93
+ ):
94
+ blob = get_blob(
95
+ blob_name="direct/path.json",
96
+ placeholders={"user_id": "123", "basename": "ignored.json"},
97
+ )
98
+ assert blob.name == "direct/path.json"
99
+ mock_bucket.blob.assert_called_once_with("direct/path.json")
100
+
101
+
102
+ def test_get_blob_with_missing_placeholder():
103
+ """Test error when placeholder is missing."""
104
+ settings_manager.user_config = {
105
+ "default": {
106
+ "bucket_name": "test-bucket",
107
+ "blob_name_pattern": "users/{user_id}/files/{basename}",
108
+ }
109
+ }
110
+
111
+ with pytest.raises(
112
+ ValueError,
113
+ match=r"Missing placeholder 'basename' in blob_name_pattern: "
114
+ r"users/\{user_id\}/files/\{basename\}",
115
+ ):
116
+ get_blob(placeholders={"user_id": "123"})
117
+
118
+
119
+ def test_get_blob_without_blob_name_and_pattern():
120
+ """Test error when neither blob_name nor blob_name_pattern is provided."""
121
+ settings_manager.user_config = {
122
+ "default": {
123
+ "bucket_name": "test-bucket",
124
+ }
125
+ }
126
+
127
+ with pytest.raises(
128
+ ValueError,
129
+ match="blob_name is not provided, placeholders are not provided, "
130
+ "and blob_name_pattern is not set in settings",
131
+ ):
132
+ get_blob()
133
+
134
+
135
+ def test_get_blob_with_placeholders_but_no_pattern():
136
+ """Test error when placeholders are provided but pattern is not set."""
137
+ settings_manager.user_config = {
138
+ "default": {
139
+ "bucket_name": "test-bucket",
140
+ }
141
+ }
142
+
143
+ with pytest.raises(
144
+ ValueError,
145
+ match="placeholders provided but blob_name_pattern is not set in settings",
146
+ ):
147
+ get_blob(placeholders={"user_id": "123"})
148
+
149
+
150
+ def test_get_blob_with_custom_config_key():
151
+ """Test get_blob with custom config_key."""
152
+ settings_manager.user_config = {
153
+ "custom": {
154
+ "bucket_name": "custom-bucket",
155
+ "blob_name_pattern": "custom/path.json",
156
+ }
157
+ }
158
+
159
+ mock_bucket = MagicMock()
160
+ mock_blob = MagicMock()
161
+ mock_blob.name = "custom/path.json"
162
+ mock_bucket.blob.return_value = mock_blob
163
+
164
+ with patch(
165
+ "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
166
+ return_value=mock_bucket,
167
+ ) as mock_get_bucket:
168
+ blob = get_blob(config_key="custom")
169
+ assert blob.name == "custom/path.json"
170
+ mock_get_bucket.assert_called_once_with("custom", auth_config_key=None)
171
+
172
+
173
+ def test_get_blob_with_auth_config_key():
174
+ """Test get_blob with custom auth_config_key."""
175
+ settings_manager.user_config = {
176
+ "default": {
177
+ "bucket_name": "test-bucket",
178
+ "blob_name_pattern": "data.json",
179
+ }
180
+ }
181
+
182
+ mock_bucket = MagicMock()
183
+ mock_blob = MagicMock()
184
+ mock_blob.name = "data.json"
185
+ mock_bucket.blob.return_value = mock_blob
186
+
187
+ with patch(
188
+ "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
189
+ return_value=mock_bucket,
190
+ ) as mock_get_bucket:
191
+ blob = get_blob(auth_config_key="custom_auth")
192
+ assert blob.name == "data.json"
193
+ mock_get_bucket.assert_called_once_with(None, auth_config_key="custom_auth")
194
+
195
+
196
+ def test_get_blob_with_complex_pattern():
197
+ """Test get_blob with complex multi-level pattern."""
198
+ settings_manager.user_config = {
199
+ "default": {
200
+ "bucket_name": "test-bucket",
201
+ "blob_name_pattern": "web/{user_id}/{agent_id}/files/{basename}",
202
+ }
203
+ }
204
+
205
+ mock_bucket = MagicMock()
206
+ mock_blob = MagicMock()
207
+ mock_blob.name = "web/user123/agent456/files/document.pdf"
208
+ mock_bucket.blob.return_value = mock_blob
209
+
210
+ with patch(
211
+ "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
212
+ return_value=mock_bucket,
213
+ ):
214
+ blob = get_blob(
215
+ placeholders={
216
+ "user_id": "user123",
217
+ "agent_id": "agent456",
218
+ "basename": "document.pdf",
219
+ }
220
+ )
221
+ assert blob.name == "web/user123/agent456/files/document.pdf"
222
+ mock_bucket.blob.assert_called_once_with(
223
+ "web/user123/agent456/files/document.pdf"
224
+ )
@@ -1,28 +0,0 @@
1
- from typing import Any
2
-
3
- from google.cloud import storage # type: ignore[import-untyped]
4
-
5
- from ._get_bucket import get_bucket
6
- from .settings import settings_manager
7
-
8
-
9
- def get_blob(
10
- blob_name: str | None = None,
11
- *,
12
- config_key: str | None = None,
13
- auth_config_key: str | None = None,
14
- **kwargs: Any,
15
- ) -> storage.Blob:
16
- settings = settings_manager.get_settings_by_key(config_key)
17
-
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")
20
-
21
- if blob_name is None:
22
- blob_name = settings.blob_name
23
-
24
- if settings.blob_name_prefix:
25
- blob_name = f"{settings.blob_name_prefix}/{blob_name}"
26
-
27
- bucket = get_bucket(config_key, auth_config_key=auth_config_key, **kwargs)
28
- return bucket.blob(blob_name)
@@ -1,13 +0,0 @@
1
- from pydantic_settings import BaseSettings
2
- from pydantic_settings_manager import SettingsManager
3
-
4
-
5
- class GoogleCloudStorageSettings(BaseSettings):
6
- bucket_name: str | None = None
7
-
8
- blob_name_prefix: str | None = None
9
-
10
- blob_name: str | None = None
11
-
12
-
13
- settings_manager = SettingsManager(GoogleCloudStorageSettings, multi=True)
@@ -1,175 +0,0 @@
1
- from unittest.mock import MagicMock, patch
2
-
3
- import pytest
4
-
5
- from kiarina.lib.google.cloud_storage import get_blob, settings_manager
6
-
7
-
8
- def test_get_blob():
9
- # Setup settings
10
- settings_manager.user_config = {
11
- "default": {
12
- "bucket_name": "test-bucket",
13
- "blob_name": "test-blob.txt",
14
- }
15
- }
16
-
17
- # Mock get_bucket and blob
18
- mock_bucket = MagicMock()
19
- mock_blob = MagicMock()
20
- mock_blob.name = "test-blob.txt"
21
- mock_bucket.blob.return_value = mock_blob
22
-
23
- with patch(
24
- "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
25
- return_value=mock_bucket,
26
- ):
27
- blob = get_blob()
28
- assert blob.name == "test-blob.txt"
29
-
30
- # Verify blob was called with correct blob name
31
- mock_bucket.blob.assert_called_once_with("test-blob.txt")
32
-
33
-
34
- def test_get_blob_with_blob_name_parameter():
35
- # Setup settings
36
- settings_manager.user_config = {
37
- "default": {
38
- "bucket_name": "test-bucket",
39
- }
40
- }
41
-
42
- # Mock get_bucket and blob
43
- mock_bucket = MagicMock()
44
- mock_blob = MagicMock()
45
- mock_blob.name = "custom-blob.txt"
46
- mock_bucket.blob.return_value = mock_blob
47
-
48
- with patch(
49
- "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
50
- return_value=mock_bucket,
51
- ):
52
- blob = get_blob(blob_name="custom-blob.txt")
53
- assert blob.name == "custom-blob.txt"
54
-
55
- # Verify blob was called with custom blob name
56
- mock_bucket.blob.assert_called_once_with("custom-blob.txt")
57
-
58
-
59
- def test_get_blob_with_blob_name_prefix():
60
- # Setup settings with blob_name_prefix
61
- settings_manager.user_config = {
62
- "default": {
63
- "bucket_name": "test-bucket",
64
- "blob_name_prefix": "prefix",
65
- "blob_name": "test-blob.txt",
66
- }
67
- }
68
-
69
- # Mock get_bucket and blob
70
- mock_bucket = MagicMock()
71
- mock_blob = MagicMock()
72
- mock_blob.name = "prefix/test-blob.txt"
73
- mock_bucket.blob.return_value = mock_blob
74
-
75
- with patch(
76
- "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
77
- return_value=mock_bucket,
78
- ):
79
- blob = get_blob()
80
- assert blob.name == "prefix/test-blob.txt"
81
-
82
- # Verify blob was called with prefixed blob name
83
- mock_bucket.blob.assert_called_once_with("prefix/test-blob.txt")
84
-
85
-
86
- def test_get_blob_with_blob_name_prefix_and_parameter():
87
- # Setup settings with blob_name_prefix
88
- settings_manager.user_config = {
89
- "default": {
90
- "bucket_name": "test-bucket",
91
- "blob_name_prefix": "prefix",
92
- }
93
- }
94
-
95
- # Mock get_bucket and blob
96
- mock_bucket = MagicMock()
97
- mock_blob = MagicMock()
98
- mock_blob.name = "prefix/custom-blob.txt"
99
- mock_bucket.blob.return_value = mock_blob
100
-
101
- with patch(
102
- "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
103
- return_value=mock_bucket,
104
- ):
105
- blob = get_blob(blob_name="custom-blob.txt")
106
- assert blob.name == "prefix/custom-blob.txt"
107
-
108
- # Verify blob was called with prefixed custom blob name
109
- mock_bucket.blob.assert_called_once_with("prefix/custom-blob.txt")
110
-
111
-
112
- def test_get_blob_without_blob_name():
113
- # Setup settings without blob_name
114
- settings_manager.user_config = {
115
- "default": {
116
- "bucket_name": "test-bucket",
117
- }
118
- }
119
-
120
- with pytest.raises(
121
- ValueError, match="blob_name is not set in the settings and not provided"
122
- ):
123
- get_blob()
124
-
125
-
126
- def test_get_blob_with_custom_config_key():
127
- # Setup settings with custom config key
128
- settings_manager.user_config = {
129
- "custom": {
130
- "bucket_name": "custom-bucket",
131
- "blob_name": "custom-blob.txt",
132
- }
133
- }
134
-
135
- # Mock get_bucket and blob
136
- mock_bucket = MagicMock()
137
- mock_blob = MagicMock()
138
- mock_blob.name = "custom-blob.txt"
139
- mock_bucket.blob.return_value = mock_blob
140
-
141
- with patch(
142
- "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
143
- return_value=mock_bucket,
144
- ) as mock_get_bucket:
145
- blob = get_blob(config_key="custom")
146
- assert blob.name == "custom-blob.txt"
147
-
148
- # Verify get_bucket was called with custom config key and no auth_config_key
149
- mock_get_bucket.assert_called_once_with("custom", auth_config_key=None)
150
-
151
-
152
- def test_get_blob_with_auth_config_key():
153
- # Setup settings
154
- settings_manager.user_config = {
155
- "default": {
156
- "bucket_name": "test-bucket",
157
- "blob_name": "test-blob.txt",
158
- }
159
- }
160
-
161
- # Mock get_bucket and blob
162
- mock_bucket = MagicMock()
163
- mock_blob = MagicMock()
164
- mock_blob.name = "test-blob.txt"
165
- mock_bucket.blob.return_value = mock_blob
166
-
167
- with patch(
168
- "kiarina.lib.google.cloud_storage._get_blob.get_bucket",
169
- return_value=mock_bucket,
170
- ) as mock_get_bucket:
171
- blob = get_blob(auth_config_key="custom_auth")
172
- assert blob.name == "test-blob.txt"
173
-
174
- # Verify get_bucket was called with custom auth config key
175
- mock_get_bucket.assert_called_once_with(None, auth_config_key="custom_auth")