Skip to content

Commit 9653006

Browse files
authored
Merge pull request #11 from imagekit-developer/url_refactor
Refactor url generation logic
2 parents f71390d + be5b5b2 commit 9653006

File tree

4 files changed

+88
-55
lines changed

4 files changed

+88
-55
lines changed

imagekitio/constants/defaults.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,12 @@ class Default(enum.Enum):
88
DEFAULT_TRANSFORMATION_POSITION,
99
QUERY_TRANSFORMATION_POSITION,
1010
]
11-
DEFAULT_TIMESTAMP = "9999999999"
11+
DEFAULT_TIMESTAMP = 9999999999
12+
SDK_VERSION_PARAMETER = "ik-sdk-version"
1213
SDK_VERSION = "python-2.2.4"
14+
TRANSFORMATION_PARAMETER = "tr"
15+
CHAIN_TRANSFORM_DELIMITER = ":"
16+
TRANSFORM_DELIMITER = ","
17+
TRANSFORM_KEY_VALUE_DELIMITER = "-"
18+
SIGNATURE_PARAMETER = "ik-s"
19+
TIMESTAMP_PARAMETER = "ik-t"

imagekitio/url.py

Lines changed: 47 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,8 @@
1111

1212
from .constants import ERRORS
1313

14-
TRANSFORMATION_PARAMETER = "tr"
15-
DEFAULT_TRANSFORMATION_POSITION = "path"
16-
QUERY_TRANSFORMATION_POSITION = "query"
17-
CHAIN_TRANSFORM_DELIMITER = ":"
18-
TRANSFORM_DELIMITER = ","
19-
TRANSFORM_KEY_VALUE_DELIMITER = "-"
2014

21-
SIGNATURE_PARAMETER = "ik-s"
22-
TIMESTAMP_PARAMETER = "ik-t"
23-
DEFAULT_TIMESTAMP = "9999999999"
24-
25-
26-
class Url(object):
15+
class Url:
2716
"""
2817
Url class holds the request and related methods
2918
to generate url(signed and unsigned)
@@ -42,44 +31,47 @@ def build_url(self, options: dict) -> str:
4231
builds url for from all options,
4332
"""
4433

34+
# important to strip the trailing slashes. later logic assumes no trailing slashes.
4535
path = options.get("path", "").strip("/")
4636
src = options.get("src", "").strip("/")
4737
url_endpoint = options.get("url_endpoint", "").strip("/")
4838
transformation_str = self.transformation_to_str(options.get("transformation"))
49-
transformation_position = options.get("transformation_position") or DEFAULT_TRANSFORMATION_POSITION
39+
transformation_position = options.get("transformation_position", Default.DEFAULT_TRANSFORMATION_POSITION.value)
5040

5141
if transformation_position not in Default.VALID_TRANSFORMATION_POSITION.value:
5242
raise ValueError(ERRORS.INVALID_TRANSFORMATION_POSITION.value)
5343

54-
if (path is "" and src is ""):
44+
if (path == "" and src == ""):
5545
return ""
5646

57-
if src:
58-
temp_url = src.strip("/")
59-
transformation_position = QUERY_TRANSFORMATION_POSITION
60-
else:
47+
# if path is present then it is given priority over src parameter
48+
if path:
6149
if transformation_position == "path":
6250
temp_url = "{}/{}:{}/{}".format(
63-
url_endpoint.strip("/"),
64-
TRANSFORMATION_PARAMETER,
51+
url_endpoint,
52+
Default.TRANSFORMATION_PARAMETER.value,
6553
transformation_str.strip("/"),
66-
path.strip("/")
54+
path
6755
)
6856
else:
6957
temp_url = "{}/{}".format(
70-
url_endpoint.strip("/"),
71-
path.strip("/")
58+
url_endpoint,
59+
path
7260
)
61+
else:
62+
temp_url = src
63+
# if src parameter is used, then we force transformation position in query
64+
transformation_position = Default.QUERY_TRANSFORMATION_POSITION.value
7365

74-
url_object = urlparse(temp_url.strip("/"))
66+
url_object = urlparse(temp_url)
7567

7668
query_params = dict(parse_qsl(url_object.query))
7769
query_params.update(options.get("query_parameters", {}))
78-
if transformation_position == QUERY_TRANSFORMATION_POSITION:
79-
query_params.update({"tr": transformation_str})
80-
query_params.update({"ik-sdk-version": Default.SDK_VERSION.value})
70+
if transformation_position == Default.QUERY_TRANSFORMATION_POSITION.value:
71+
query_params.update({Default.TRANSFORMATION_PARAMETER.value: transformation_str})
72+
query_params.update({Default.SDK_VERSION_PARAMETER.value: Default.SDK_VERSION.value})
8173

82-
# Update query params
74+
# Update query params in the url
8375
url_object = url_object._replace(query=urlencode(query_params))
8476

8577
if options.get("signed"):
@@ -92,39 +84,41 @@ def build_url(self, options: dict) -> str:
9284
url_endpoint=url_endpoint,
9385
expiry_timestamp=expiry_timestamp,
9486
)
95-
query_params.update({TIMESTAMP_PARAMETER: expiry_timestamp, SIGNATURE_PARAMETER: url_signature})
87+
88+
"""
89+
If the expire_seconds parameter is specified then the output URL contains
90+
ik-t parameter (unix timestamp seconds when the URL expires) and
91+
the signature contains the timestamp for computation.
92+
93+
If not present, then no ik-t parameter and the value 9999999999 is used.
94+
"""
95+
if expire_seconds:
96+
query_params.update({Default.TIMESTAMP_PARAMETER.value: expiry_timestamp, Default.SIGNATURE_PARAMETER.value: url_signature})
97+
else:
98+
query_params.update({Default.SIGNATURE_PARAMETER.value: url_signature})
99+
96100
# Update signature related query params
97101
url_object = url_object._replace(query=urlencode(query_params))
98102

99103
return url_object.geturl()
100104

101105
@staticmethod
102-
def get_signature_timestamp(seconds: int = None) -> int:
106+
def get_signature_timestamp(expiry_seconds: int = None) -> int:
103107
"""
104-
this function returns either default time stamp
105-
or current unix time and expiry seconds to get
106-
signature time stamp
108+
this function returns the signature timestamp to be used
109+
with the generated url.
110+
If expiry_seconds is provided, it returns expiry_seconds added
111+
to the current unix time, otherwise the default time stamp
112+
is returned.
107113
"""
108-
if not seconds:
114+
if not expiry_seconds:
109115
return Default.DEFAULT_TIMESTAMP.value
110116
current_timestamp = int(dt.now().strftime("%s"))
111117

112-
return current_timestamp + seconds
113-
114-
@staticmethod
115-
def prepare_dict_for_unparse(url_dict: dict) -> dict:
116-
"""
117-
remove and add required back slash of 'netloc' and 'path'
118-
to parse it properly, urllib.parse.unparse() function can't
119-
create url properly if path doesn't have '/' at the start
120-
"""
121-
url_dict["netloc"] = url_dict["netloc"].rstrip("/")
122-
url_dict["path"] = "/" + url_dict["path"].strip("/")
123-
124-
return url_dict
118+
return current_timestamp + expiry_seconds
125119

126120
@staticmethod
127-
def get_signature(private_key, url, url_endpoint, expiry_timestamp) -> str:
121+
def get_signature(private_key, url, url_endpoint, expiry_timestamp : int) -> str:
128122
""""
129123
create signature(hashed hex key) from
130124
private_key, url, url_endpoint and expiry_timestamp
@@ -133,8 +127,8 @@ def get_signature(private_key, url, url_endpoint, expiry_timestamp) -> str:
133127
if url_endpoint[-1] != '/':
134128
url_endpoint += '/'
135129

136-
if isinstance(expiry_timestamp, int) and expiry_timestamp < 1:
137-
expiry_timestamp = DEFAULT_TIMESTAMP
130+
if expiry_timestamp < 1:
131+
expiry_timestamp = Default.DEFAULT_TIMESTAMP.value
138132

139133
replaced_url = url.replace(url_endpoint, "") + str(expiry_timestamp)
140134

@@ -187,12 +181,12 @@ def transformation_to_str(transformation):
187181
parsed_transform_step.append(
188182
"{}{}{}".format(
189183
transform_key,
190-
TRANSFORM_KEY_VALUE_DELIMITER,
184+
Default.TRANSFORM_KEY_VALUE_DELIMITER.value,
191185
transformation[i][key],
192186
)
193187
)
194188

195189
parsed_transforms.append(
196-
TRANSFORM_DELIMITER.join(parsed_transform_step))
190+
Default.TRANSFORM_DELIMITER.value.join(parsed_transform_step))
197191

198-
return CHAIN_TRANSFORM_DELIMITER.join(parsed_transforms)
192+
return Default.CHAIN_TRANSFORM_DELIMITER.value.join(parsed_transforms)

imagekitio/utils/formatter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def request_formatter(data: dict) -> dict:
3131

3232

3333
def camel_dict_to_snake_dict(data: dict) -> dict:
34+
"""Convert the keys of dictionary from camel case to snake case
35+
"""
3436
return {camel_to_snake(key): val for key, val in data.items()}
3537

3638

tests/test_generate_url.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,20 @@ def test_generate_url_with_path_and_signed_in_proper_form(self):
192192
}
193193

194194
url = self.client.url(options)
195-
self.assertIn("ik-s", url)
195+
self.assertIn(Default.SIGNATURE_PARAMETER.value, url)
196+
197+
def test_generate_url_signed_without_expiry_does_not_have_timestamp_parameter(self):
198+
"""
199+
Check query params does not contain timestamp parameter if expire_seconds isn't specified.
200+
"""
201+
options = {
202+
"path": "/test-signed-url.jpg",
203+
"signed": True,
204+
"transformation": [{"width": 100}],
205+
}
206+
207+
url = self.client.url(options)
208+
self.assertNotIn(Default.TIMESTAMP_PARAMETER.value, url)
196209

197210
def test_url_with_new_transformation_returns_as_it_is(self):
198211
options = {
@@ -370,6 +383,23 @@ def test_url_signed_with_expire_in_seconds(self):
370383
url = self.client.url(options)
371384
self.assertIn("ik-t", url)
372385

386+
def test_generate_url_with_path_and_src_uses_path(self):
387+
"""
388+
In case when both path and src fields are provided, the `path` should be preferred
389+
"""
390+
options = {
391+
"path": "/default-image.jpg",
392+
"src": "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg",
393+
"transformation": [{"height": "300", "width": "400"}],
394+
}
395+
url = self.client.url(options)
396+
self.assertEqual(
397+
url,
398+
"https://test-domain.com/test-endpoint/tr:h-300,w-400/default-image.jpg?ik-sdk-version={}".format(
399+
Default.SDK_VERSION.value
400+
),
401+
)
402+
373403
def test_get_signature_with_100_expire_seconds(self):
374404
url = "https://test-domain.com/test-endpoint/tr:w-100/test-signed-url.png"
375405
signature = self.client.url_obj.get_signature(

0 commit comments

Comments
 (0)