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.
- {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/CHANGELOG.md +11 -0
- {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/PKG-INFO +165 -21
- {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/README.md +164 -20
- {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/pyproject.toml +1 -1
- kiarina_lib_google_cloud_storage-1.6.2/src/kiarina/lib/google/cloud_storage/_get_blob.py +78 -0
- kiarina_lib_google_cloud_storage-1.6.2/src/kiarina/lib/google/cloud_storage/settings.py +19 -0
- kiarina_lib_google_cloud_storage-1.6.2/tests/test_get_blob.py +224 -0
- kiarina_lib_google_cloud_storage-1.6.1/src/kiarina/lib/google/cloud_storage/_get_blob.py +0 -28
- kiarina_lib_google_cloud_storage-1.6.1/src/kiarina/lib/google/cloud_storage/settings.py +0 -13
- kiarina_lib_google_cloud_storage-1.6.1/tests/test_get_blob.py +0 -175
- {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/.gitignore +0 -0
- {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/.vscode/settings.json +0 -0
- {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
- {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
- {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
- {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
- {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/tests/__init__.py +0 -0
- {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/tests/conftest.py +0 -0
- {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/tests/test_get_bucket.py +0 -0
- {kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/tests/test_get_storage_client.py +0 -0
{kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/CHANGELOG.md
RENAMED
@@ -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.
|
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
|
68
|
+
# ✅ Application code only provides variables
|
68
69
|
from kiarina.lib.google.cloud_storage import get_blob
|
69
70
|
|
70
|
-
#
|
71
|
-
blob = get_blob(
|
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
|
-
"
|
230
|
+
"blob_name_pattern": "production/v1/{basename}"
|
110
231
|
}
|
111
232
|
}
|
112
233
|
|
113
234
|
# Application code - clean and simple
|
114
|
-
blob = get_blob(
|
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
|
-
| `
|
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`:
|
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
|
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
|
-
#
|
525
|
-
blob = get_blob(blob_name="data.json")
|
526
|
-
|
527
|
-
#
|
528
|
-
# If
|
529
|
-
|
530
|
-
|
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
|
-
|
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
|
40
|
+
# ✅ Application code only provides variables
|
40
41
|
from kiarina.lib.google.cloud_storage import get_blob
|
41
42
|
|
42
|
-
#
|
43
|
-
blob = get_blob(
|
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
|
-
"
|
202
|
+
"blob_name_pattern": "production/v1/{basename}"
|
82
203
|
}
|
83
204
|
}
|
84
205
|
|
85
206
|
# Application code - clean and simple
|
86
|
-
blob = get_blob(
|
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
|
-
| `
|
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`:
|
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
|
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
|
-
#
|
497
|
-
blob = get_blob(blob_name="data.json")
|
498
|
-
|
499
|
-
#
|
500
|
-
# If
|
501
|
-
|
502
|
-
|
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
|
-
|
650
|
+
placeholders={"basename": "data.json"},
|
507
651
|
config_key="production",
|
508
652
|
auth_config_key="prod_auth"
|
509
653
|
)
|
@@ -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")
|
{kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/.gitignore
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/tests/__init__.py
RENAMED
File without changes
|
{kiarina_lib_google_cloud_storage-1.6.1 → kiarina_lib_google_cloud_storage-1.6.2}/tests/conftest.py
RENAMED
File without changes
|
File without changes
|
File without changes
|