Skip to content

Commit 8a265c0

Browse files
Merge pull request #116 from dotflow-io/feature/115
⚙️ FEATURE-#115: Add StorageGCS provider for Google Cloud Storage persistence
2 parents b4ef35b + 28f29be commit 8a265c0

9 files changed

Lines changed: 658 additions & 26 deletions

File tree

docs/nav/reference/storage-gcs.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# StorageGCS
2+
3+
::: dotflow.providers.storage_gcs.StorageGCS

docs/nav/tutorial/storage-gcs.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Storage GCS
2+
3+
Persists task output to Google Cloud Storage. Good for serverless and cloud-native workflows on GCP.
4+
5+
/// note
6+
Requires `pip install dotflow[gcp]`
7+
///
8+
9+
## Example
10+
11+
{* ./docs_src/storage/storage_gcs.py ln[1:34] hl[2,16:22] *}
12+
13+
## Authentication
14+
15+
`StorageGCS` uses Application Default Credentials (ADC):
16+
17+
1. Environment variable: `GOOGLE_APPLICATION_CREDENTIALS` pointing to a service account JSON
18+
2. `gcloud auth application-default login` for local development
19+
3. Service account: automatic on Cloud Run, Cloud Functions, GKE
20+
21+
No credentials are needed in code — the GCP client handles it transparently.
22+
23+
## References
24+
25+
- [StorageGCS](https://dotflow-io.github.io/dotflow/nav/reference/storage-gcs/)

docs_src/storage/storage_gcs.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from dotflow import Config, DotFlow, action
2+
from dotflow.providers import StorageGCS
3+
4+
5+
@action
6+
def step_one():
7+
return {"message": "hello from GCS"}
8+
9+
10+
@action
11+
def step_two(previous_context):
12+
print(previous_context.storage)
13+
return "ok"
14+
15+
16+
config = Config(
17+
storage=StorageGCS(
18+
bucket="dotflow-io-bucket",
19+
prefix="workflows/",
20+
project="etl-test",
21+
)
22+
)
23+
24+
25+
def main():
26+
workflow = DotFlow(config=config)
27+
28+
workflow.task.add(step=step_one)
29+
workflow.task.add(step=step_two)
30+
workflow.start()
31+
32+
return workflow
33+
34+
35+
if __name__ == "__main__":
36+
main()

dotflow/providers/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"StorageDefault",
1414
"StorageFile",
1515
"StorageS3",
16+
"StorageGCS",
1617
]
1718

1819

@@ -21,4 +22,10 @@ def __getattr__(name):
2122
from dotflow.providers.storage_s3 import StorageS3
2223

2324
return StorageS3
25+
26+
if name == "StorageGCS":
27+
from dotflow.providers.storage_gcs import StorageGCS
28+
29+
return StorageGCS
30+
2431
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

dotflow/providers/storage_gcs.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Storage GCS"""
2+
3+
from collections.abc import Callable
4+
from json import dumps, loads
5+
from typing import Any
6+
7+
from dotflow.abc.storage import Storage
8+
from dotflow.core.context import Context
9+
from dotflow.core.exception import ModuleNotFound
10+
11+
12+
class StorageGCS(Storage):
13+
"""
14+
Import:
15+
You can import the **StorageGCS** class directly from dotflow providers:
16+
17+
from dotflow.providers import StorageGCS
18+
19+
Example:
20+
`class` dotflow.providers.storage_gcs.StorageGCS
21+
22+
from dotflow import Config
23+
from dotflow.providers import StorageGCS
24+
25+
config = Config(
26+
storage=StorageGCS(
27+
bucket="my-dotflow-bucket",
28+
prefix="workflows/",
29+
project="my-gcp-project"
30+
)
31+
)
32+
33+
Args:
34+
bucket (str): GCS bucket name.
35+
36+
prefix (str): Key prefix for all stored objects.
37+
38+
project (str): GCP project ID. Defaults to ADC project.
39+
"""
40+
41+
def __init__(
42+
self,
43+
*args,
44+
bucket: str,
45+
prefix: str = "dotflow/",
46+
project: str = None,
47+
**kwargs,
48+
):
49+
try:
50+
from google.api_core.exceptions import NotFound
51+
from google.cloud import storage as gcs
52+
except ImportError:
53+
raise ModuleNotFound(
54+
module="google-cloud-storage",
55+
library="dotflow[gcp]",
56+
) from None
57+
58+
self._not_found = NotFound
59+
self.client = gcs.Client(project=project)
60+
self.bucket_obj = self.client.bucket(bucket)
61+
self.bucket_obj.reload()
62+
self.prefix = prefix
63+
64+
def post(self, key: str, context: Context) -> None:
65+
task_context = []
66+
67+
if isinstance(context.storage, list):
68+
for item in context.storage:
69+
if isinstance(item, Context):
70+
task_context.append(self._dumps(storage=item.storage))
71+
else:
72+
task_context.append(self._dumps(storage=context.storage))
73+
74+
self._write(key=key, data=task_context)
75+
76+
def get(self, key: str) -> Context:
77+
task_context = self._read(key)
78+
79+
if len(task_context) == 0:
80+
return Context()
81+
82+
if len(task_context) == 1:
83+
return self._loads(storage=task_context[0])
84+
85+
contexts = Context(storage=[])
86+
for context in task_context:
87+
contexts.storage.append(self._loads(storage=context))
88+
89+
return contexts
90+
91+
def key(self, task: Callable):
92+
return f"{task.workflow_id}-{task.task_id}"
93+
94+
def _read(self, key: str) -> list:
95+
blob = self.bucket_obj.blob(f"{self.prefix}{key}")
96+
try:
97+
data = blob.download_as_text()
98+
return loads(data)
99+
except self._not_found:
100+
return []
101+
102+
def _write(self, key: str, data: list) -> None:
103+
blob = self.bucket_obj.blob(f"{self.prefix}{key}")
104+
blob.upload_from_string(
105+
dumps(data),
106+
content_type="application/json",
107+
)
108+
109+
def _loads(self, storage: Any) -> Context:
110+
try:
111+
return Context(storage=loads(storage))
112+
except Exception:
113+
return Context(storage=storage)
114+
115+
def _dumps(self, storage: Any) -> str:
116+
try:
117+
return dumps(storage)
118+
except TypeError:
119+
return str(storage)

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ nav:
130130
- nav/tutorial/storage-default.md
131131
- nav/tutorial/storage-file.md
132132
- nav/tutorial/storage-s3.md
133+
- nav/tutorial/storage-gcs.md
133134
- nav/tutorial/provider-notify.md
134135
- nav/tutorial/provider-log.md
135136
- Tutorial - User Guide:
@@ -187,6 +188,7 @@ nav:
187188
- nav/reference/storage-init.md
188189
- nav/reference/storage-file.md
189190
- nav/reference/storage-s3.md
191+
- nav/reference/storage-gcs.md
190192
- Instance:
191193
- nav/reference/task-instance.md
192194
- nav/reference/context-instance.md

0 commit comments

Comments
 (0)