Skip to content

Commit d8a3dcf

Browse files
authored
Merge pull request #74 from octue/task-utils
Add utilities to tasks module
2 parents 86b42bd + 58691b6 commit d8a3dcf

File tree

3 files changed

+142
-2
lines changed

3 files changed

+142
-2
lines changed

django_gcp/tasks/__init__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
from .manager import TaskManager
22
from .tasks import OnDemandTask, PeriodicTask, SubscriberTask
3-
3+
from .utils import BlobFieldMixin, get_blob, get_blob_name, get_path, get_signed_url, upload_blob
44

55
__all__ = (
66
"PeriodicTask",
77
"SubscriberTask",
88
"OnDemandTask",
99
"TaskManager",
10+
"BlobFieldMixin",
11+
"get_blob",
12+
"get_blob_name",
13+
"get_path",
14+
"get_signed_url",
15+
"upload_blob",
1016
)

django_gcp/tasks/utils.py

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from datetime import timedelta
2+
from os.path import split
3+
4+
from google.cloud.storage.blob import Blob
5+
6+
7+
def upload_blob(
8+
instance,
9+
field_name,
10+
local_path,
11+
destination_path=None,
12+
attributes=None,
13+
allow_overwrite=False,
14+
existing_path=None,
15+
):
16+
"""Upload a file to the cloud store, using the instance and field name to determine the store details
17+
18+
You might use this utility to upload fixtue files for integration tests, as part of
19+
a migration, or as part of a function creating local files. The field's own logic for generating paths
20+
is used by default, although this can be overridden.
21+
22+
Returns the field value so you can use this to construct instances directly:
23+
24+
# Directly set the instance field without processing the blob ingress
25+
with override_settings(GCP_STORAGE_OVERRIDE_BLOBFIELD_VALUE=True):
26+
instance.field_name = upload_blob(...)
27+
28+
:param django.db.Model instance: An instance of a django Model which has a BlobField
29+
:param str field_name: The name of the BlobField attribute on the instance
30+
:param str local_path: The path to the file to upload
31+
:param str destination_path: The path to upload the file to. If None, the remote path will be generated
32+
from the BlobField. If setting this value, take care to override the value of the field on the instance
33+
so it's path matches; this is not updated for you.
34+
:param dict attributes: A dictionary of attributes to set on the blob eg content type
35+
:param bool allow_overwrite: If true, allows existing blobs at the path to be overwritten. If destination_path is not given, this is provided to the get_destination_path callback (and may be overridden by that callback per its specification)
36+
:param str existing_path: If destination_path is None, this is provided to the get_destination_path callback to simulate behaviour where there is an existing path
37+
"""
38+
# Get the field (which
39+
field = instance._meta.get_field(field_name)
40+
if destination_path is None:
41+
destination_path, allow_overwrite = field.get_destination_path(
42+
instance,
43+
original_name=local_path,
44+
attributes=attributes,
45+
allow_overwrite=allow_overwrite,
46+
existing_path=existing_path,
47+
bucket=field.storage.bucket,
48+
)
49+
50+
# If not allowing overwrite, set generation matching constraints to prevent it
51+
if_generation_match = None if allow_overwrite else 0
52+
53+
# Attributes must be a dict by default
54+
attributes = attributes or {}
55+
56+
# Upload the file
57+
Blob(destination_path, bucket=field.storage.bucket).upload_from_filename(
58+
local_path, if_generation_match=if_generation_match, **attributes
59+
)
60+
61+
# Return the field value
62+
return {"path": destination_path}
63+
64+
65+
def get_path(instance, field_name):
66+
"""Get the path of the blob in the object store"""
67+
field_value = getattr(instance, field_name)
68+
return field_value.get("path", None) if field_value is not None else None
69+
70+
71+
def get_blob(instance, field_name):
72+
"""Get a blob from a model instance containing a BlobField
73+
74+
This allows you to download the blob to a local file. For example:
75+
76+
```py
77+
blob = get_blob(mymodel, "my_field_name")
78+
79+
logger.info("Downloading file %s from bucket %s", blob.name, blob.bucket.name)
80+
81+
# Download the blob to the temporary directory
82+
blob_file_name = os.path.split(blob.name)[-1]
83+
blob.download_to_filename(blob_file_name)
84+
```
85+
86+
:param django.db.Model instance: An instance of a django Model which has a BlobField
87+
:param str field_name: The name of the BlobField attribute on the instance
88+
"""
89+
path = get_path(instance, field_name)
90+
if path is not None:
91+
field = instance._meta.get_field(field_name)
92+
return field.storage.bucket.blob(path)
93+
94+
95+
def get_blob_name(instance, field_name):
96+
"""Get the name of the blob including its extension (absent any path)
97+
98+
The name is the object path absent any folder prefixes,
99+
eg if blob is located at path `mystuff/1234/myfile.txt` the name
100+
is `myfile.txt`
101+
"""
102+
path = get_path(instance, field_name)
103+
if path is not None:
104+
return split(path)[-1]
105+
106+
107+
def get_signed_url(instance, field_name, expiration=None):
108+
"""Get a signed URL to the blob for the given model field name
109+
:param str field_name: Name of the model field (which should be a BlobField)
110+
:param Union[datetime.datetime|datetime.timedelta|None] expiration: Expiration date or duration for the URL. If None, duration defaults to 24hrs.
111+
:return str: Signed URL of the blob
112+
"""
113+
expiration = expiration or timedelta(hours=24)
114+
return get_blob(instance, field_name).generate_signed_url(expiration=expiration)
115+
116+
117+
class BlobFieldModel:
118+
"""Mixin to a model to provide extra utility methods for processing of blobs"""
119+
120+
def get_blob(self, field_name):
121+
"""Get a blob object for the given model field name"""
122+
return get_blob(self, field_name)
123+
124+
def get_blob_name(self, field_name):
125+
"""Get blob name for the given model field name"""
126+
return get_blob_name(self, field_name)
127+
128+
def get_path(self, field_name):
129+
"""Get the path of the blob in the object store for the given model field name"""
130+
return get_path(self, field_name)
131+
132+
def get_signed_url(self, field_name, expiration=None):
133+
"""Get a signed URL to the blob for the given model field name"""
134+
return get_signed_url(self, field_name, expiration)

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-gcp"
3-
version = "0.13.0"
3+
version = "0.14.0"
44
description = "Utilities to run Django on Google Cloud Platform"
55
authors = ["Tom Clark"]
66
license = "MIT"

0 commit comments

Comments
 (0)