Skip to content

Commit ecf5f03

Browse files
authored
feat: add Prefixer class to generate and parse resource names (#39)
1 parent e90caf9 commit ecf5f03

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import datetime
16+
import random
17+
import re
18+
19+
20+
_RESOURCE_DATE_FORMAT = "%Y%m%d%H%M%S"
21+
_RESOURCE_DATE_LENGTH = 4 + 2 + 2 + 2 + 2 + 2
22+
_RE_SEPARATORS = re.compile(r"[/\-\\_]")
23+
24+
25+
def _common_prefix(repo, relative_dir, separator="_"):
26+
repo = _RE_SEPARATORS.sub(separator, repo)
27+
relative_dir = _RE_SEPARATORS.sub(separator, relative_dir)
28+
return f"{repo}{separator}{relative_dir}"
29+
30+
31+
class Prefixer(object):
32+
"""Create/manage resource IDs for system testing.
33+
34+
Usage:
35+
36+
Creating resources:
37+
38+
>>> import test_utils.prefixer
39+
>>> prefixer = test_utils.prefixer.Prefixer("python-bigquery", "samples/snippets")
40+
>>> dataset_id = prefixer.create_prefix() + "my_sample"
41+
42+
Cleaning up resources:
43+
44+
>>> @pytest.fixture(scope="session", autouse=True)
45+
... def cleanup_datasets(bigquery_client: bigquery.Client):
46+
... for dataset in bigquery_client.list_datasets():
47+
... if prefixer.should_cleanup(dataset.dataset_id):
48+
... bigquery_client.delete_dataset(
49+
... dataset, delete_contents=True, not_found_ok=True
50+
"""
51+
52+
def __init__(
53+
self, repo, relative_dir, separator="_", cleanup_age=datetime.timedelta(days=1)
54+
):
55+
self._separator = separator
56+
self._cleanup_age = cleanup_age
57+
self._prefix = _common_prefix(repo, relative_dir, separator=separator)
58+
59+
def create_prefix(self) -> str:
60+
timestamp = datetime.datetime.utcnow().strftime(_RESOURCE_DATE_FORMAT)
61+
random_string = hex(random.randrange(0x1000000))[2:]
62+
return f"{self._prefix}{self._separator}{timestamp}{self._separator}{random_string}"
63+
64+
def _name_to_date(self, resource_name: str) -> datetime.datetime:
65+
start_date = len(self._prefix) + len(self._separator)
66+
date_string = resource_name[start_date : start_date + _RESOURCE_DATE_LENGTH]
67+
try:
68+
parsed_date = datetime.datetime.strptime(date_string, _RESOURCE_DATE_FORMAT)
69+
return parsed_date
70+
except ValueError:
71+
return None
72+
73+
def should_cleanup(self, resource_name: str) -> bool:
74+
yesterday = datetime.datetime.utcnow() - self._cleanup_age
75+
if not resource_name.startswith(self._prefix):
76+
return False
77+
78+
created_date = self._name_to_date(resource_name)
79+
return created_date is not None and created_date < yesterday
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import datetime
16+
import re
17+
18+
import pytest
19+
20+
import test_utils.prefixer
21+
22+
23+
class FakeDateTime(object):
24+
"""Fake datetime class since pytest can't monkeypatch attributes of
25+
built-in/extension type.
26+
"""
27+
28+
def __init__(self, fake_now):
29+
self._fake_now = fake_now
30+
31+
def utcnow(self):
32+
return self._fake_now
33+
34+
strptime = datetime.datetime.strptime
35+
36+
37+
@pytest.mark.parametrize(
38+
("repo", "relative_dir", "separator", "expected"),
39+
[
40+
(
41+
"python-bigquery",
42+
"samples/snippets",
43+
"_",
44+
"python_bigquery_samples_snippets",
45+
),
46+
("python-storage", "samples\\snippets", "-", "python-storage-samples-snippets"),
47+
],
48+
)
49+
def test_common_prefix(repo, relative_dir, separator, expected):
50+
got = test_utils.prefixer._common_prefix(repo, relative_dir, separator=separator)
51+
assert got == expected
52+
53+
54+
def test_create_prefix(monkeypatch):
55+
fake_datetime = FakeDateTime(datetime.datetime(2021, 6, 21, 3, 32, 0))
56+
monkeypatch.setattr(datetime, "datetime", fake_datetime)
57+
58+
prefixer = test_utils.prefixer.Prefixer(
59+
"python-test-utils", "tests/unit", separator="?"
60+
)
61+
got = prefixer.create_prefix()
62+
parts = got.split("?")
63+
assert len(parts) == 7
64+
assert "?".join(parts[:5]) == "python?test?utils?tests?unit"
65+
datetime_part = parts[5]
66+
assert datetime_part == "20210621033200"
67+
random_hex_part = parts[6]
68+
assert re.fullmatch("[0-9a-f]+", random_hex_part)
69+
70+
71+
@pytest.mark.parametrize(
72+
("resource_name", "separator", "expected"),
73+
[
74+
("test_utils_created_elsewhere", "_", False),
75+
("test_utils_20210620120000", "_", False),
76+
("test_utils_20210620120000_abcdef_my_name", "_", False),
77+
("test_utils_20210619120000", "_", True),
78+
("test_utils_20210619120000_abcdef_my_name", "_", True),
79+
("test?utils?created?elsewhere", "_", False),
80+
("test?utils?20210620120000", "?", False),
81+
("test?utils?20210619120000", "?", True),
82+
],
83+
)
84+
def test_should_cleanup(resource_name, separator, expected, monkeypatch):
85+
fake_datetime = FakeDateTime(datetime.datetime(2021, 6, 21, 3, 32, 0))
86+
monkeypatch.setattr(datetime, "datetime", fake_datetime)
87+
88+
prefixer = test_utils.prefixer.Prefixer("test", "utils", separator=separator)
89+
assert prefixer.should_cleanup(resource_name) == expected

0 commit comments

Comments
 (0)