kiarina-lib-google-cloud-storage 1.6.0__py3-none-any.whl → 1.6.2__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.
- kiarina/lib/google/cloud_storage/_get_blob.py +57 -7
- kiarina/lib/google/cloud_storage/settings.py +9 -3
- {kiarina_lib_google_cloud_storage-1.6.0.dist-info → kiarina_lib_google_cloud_storage-1.6.2.dist-info}/METADATA +165 -21
- {kiarina_lib_google_cloud_storage-1.6.0.dist-info → kiarina_lib_google_cloud_storage-1.6.2.dist-info}/RECORD +5 -5
- {kiarina_lib_google_cloud_storage-1.6.0.dist-info → kiarina_lib_google_cloud_storage-1.6.2.dist-info}/WHEEL +0 -0
@@ -9,20 +9,70 @@ from .settings import settings_manager
|
|
9
9
|
def get_blob(
|
10
10
|
blob_name: str | None = None,
|
11
11
|
*,
|
12
|
+
placeholders: dict[str, Any] | None = None,
|
12
13
|
config_key: str | None = None,
|
13
14
|
auth_config_key: str | None = None,
|
14
15
|
**kwargs: Any,
|
15
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
|
+
"""
|
16
44
|
settings = settings_manager.get_settings_by_key(config_key)
|
17
45
|
|
18
|
-
|
19
|
-
|
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
|
20
65
|
|
21
|
-
|
22
|
-
|
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
|
23
70
|
|
24
|
-
|
25
|
-
|
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
|
+
)
|
26
76
|
|
27
77
|
bucket = get_bucket(config_key, auth_config_key=auth_config_key, **kwargs)
|
28
|
-
return bucket.blob(
|
78
|
+
return bucket.blob(final_blob_name)
|
@@ -5,9 +5,15 @@ from pydantic_settings_manager import SettingsManager
|
|
5
5
|
class GoogleCloudStorageSettings(BaseSettings):
|
6
6
|
bucket_name: str | None = None
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
+
"""
|
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.
|
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
|
)
|
@@ -1,9 +1,9 @@
|
|
1
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=
|
2
|
+
kiarina/lib/google/cloud_storage/_get_blob.py,sha256=J8GFQIx_xQWgqvd2LscI83u6xmzS7uPBIshyUDgBZEI,2644
|
3
3
|
kiarina/lib/google/cloud_storage/_get_bucket.py,sha256=SaGIQqRb9UOJaosGf2pXduAEYscE_T21AHCNTaZtRGo,597
|
4
4
|
kiarina/lib/google/cloud_storage/_get_storage_client.py,sha256=lmBR2I9_vcyb0PfsiSz0BPFYBwQ6zVci75cmk9GUN5U,359
|
5
5
|
kiarina/lib/google/cloud_storage/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
-
kiarina/lib/google/cloud_storage/settings.py,sha256=
|
7
|
-
kiarina_lib_google_cloud_storage-1.6.
|
8
|
-
kiarina_lib_google_cloud_storage-1.6.
|
9
|
-
kiarina_lib_google_cloud_storage-1.6.
|
6
|
+
kiarina/lib/google/cloud_storage/settings.py,sha256=2ZG2AmWe6mM8wq-OHTqSneTIvoraIxh2Q1W7umLOFvI,540
|
7
|
+
kiarina_lib_google_cloud_storage-1.6.2.dist-info/METADATA,sha256=y16n-HD2RzseJezOZHujIS4xhzrds78eWEXKJK-p3-E,27462
|
8
|
+
kiarina_lib_google_cloud_storage-1.6.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
9
|
+
kiarina_lib_google_cloud_storage-1.6.2.dist-info/RECORD,,
|
File without changes
|