Skip to content

Commit 85a6d4c

Browse files
committed
feat: move create_hmac_signature from Apify SDK
1 parent a049246 commit 85a6d4c

File tree

2 files changed

+63
-0
lines changed

2 files changed

+63
-0
lines changed

src/apify_shared/utils.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from __future__ import annotations
22

33
import contextlib
4+
import hashlib
5+
import hmac
46
import io
57
import json
68
import re
9+
import string
710
from datetime import datetime, timezone
811
from enum import Enum
912
from typing import Any, TypeVar, cast
@@ -115,3 +118,38 @@ def parse(key: str, value: object) -> object:
115118
return {key: parse(key, value) for (key, value) in data.items()}
116119

117120
return data
121+
122+
123+
CHARSET = string.digits + string.ascii_letters
124+
125+
126+
def encode_base62(num: int) -> str:
127+
"""Encode the given number to base62."""
128+
if num == 0:
129+
return CHARSET[0]
130+
131+
res = ''
132+
while num > 0:
133+
num, remainder = divmod(num, 62)
134+
res = CHARSET[remainder] + res
135+
return res
136+
137+
138+
@ignore_docs
139+
def create_hmac_signature(secret_key: str, message: str) -> str:
140+
"""Generates an HMAC signature and encodes it using Base62. Base62 encoding reduces the signature length.
141+
142+
HMAC signature is truncated to 30 characters to make it shorter.
143+
144+
Args:
145+
secret_key (str): Secret key used for signing signatures
146+
message (str): Message to be signed
147+
148+
Returns:
149+
str: Base62 encoded signature
150+
"""
151+
signature = hmac.new(secret_key.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest()[:30]
152+
153+
decimal_signature = int(signature, 16)
154+
155+
return encode_base62(decimal_signature)

tests/unit/test_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from enum import Enum
66

77
from apify_shared.utils import (
8+
create_hmac_signature,
9+
encode_base62,
810
filter_out_none_values_recursively,
911
filter_out_none_values_recursively_internal,
1012
ignore_docs,
@@ -146,3 +148,26 @@ def testing_function(_a: str, _b: str) -> str:
146148
return 'dummy'
147149

148150
assert testing_function is ignore_docs(testing_function)
151+
152+
153+
def test_encode_base62() -> None:
154+
assert encode_base62(0) == '0'
155+
assert encode_base62(10) == 'a'
156+
assert encode_base62(999999999) == '15FTGf'
157+
158+
159+
# This test ensures compatibility with the JavaScript version of the same method.
160+
# https://github.com/apify/apify-shared-js/blob/master/packages/utilities/src/hmac.ts
161+
def test_create_valid_hmac_signature() -> None:
162+
# This test uses the same secret key and message as in JS tests.
163+
secret_key = 'hmac-secret-key'
164+
message = 'hmac-message-to-be-authenticated'
165+
assert create_hmac_signature(secret_key, message) == 'pcVagAsudj8dFqdlg7mG'
166+
167+
168+
def test_create_same_hmac() -> None:
169+
# This test uses the same secret key and message as in JS tests.
170+
secret_key = 'hmac-same-secret-key'
171+
message = 'hmac-same-message-to-be-authenticated'
172+
assert create_hmac_signature(secret_key, message) == 'FYMcmTIm3idXqleF1Sw5'
173+
assert create_hmac_signature(secret_key, message) == 'FYMcmTIm3idXqleF1Sw5'

0 commit comments

Comments
 (0)