From 6a66eadde3b6d619a10f92b22535075018c0c59d Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Sun, 21 Dec 2025 19:21:03 +0530 Subject: [PATCH 1/7] boto and python 3 migration Signed-off-by: Jay07GIT --- setup.py | 80 ++++--------- src/vinyldns/boto_request_signer.py | 171 +++++++++++++++++----------- tests/test_batch_change.py | 34 +++--- tests/test_membership.py | 100 ++++++++-------- tests/test_records.py | 134 +++++++++++----------- tests/test_zones.py | 55 +++++---- tox.ini | 57 +++------- 7 files changed, 321 insertions(+), 310 deletions(-) diff --git a/setup.py b/setup.py index 594fd37..d9887ed 100644 --- a/setup.py +++ b/setup.py @@ -12,75 +12,43 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import -from __future__ import print_function - -import sys from glob import glob -from os.path import basename -from os.path import splitext - -from setuptools import find_packages -from setuptools import setup -from setuptools.command.test import test as TestCommand - - -# Allows one to simply > python3 setup.py test -class PyTest(TestCommand): - user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] - - def initialize_options(self): - TestCommand.initialize_options(self) - self.pytest_args = [] +from os.path import basename, splitext - def finalize_options(self): - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True +from setuptools import find_packages, setup - def run_tests(self): - # import here, cause outside the eggs aren't loaded - import pytest - errno = pytest.main(self.pytest_args) - sys.exit(errno) - -with open('README.md') as f: +with open("README.md", encoding="utf-8") as f: long_description = f.read() + setup( - cmdclass={'test': PyTest}, - name='vinyldns-python', - version='0.9.7', - packages=find_packages('src'), - package_dir={'': 'src'}, - py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], + name="vinyldns-python", + version="0.9.7", + packages=find_packages("src"), + package_dir={"": "src"}, + py_modules=[splitext(basename(path))[0] for path in glob("src/*.py")], include_package_data=True, zip_safe=False, - url='https://github.com/vinyldns/vinyldns-python', - license='Apache Software License 2.0', - author='vinyldns', - author_email='vinyldns-core@googlegroups.com', + url="https://github.com/vinyldns/vinyldns-python", + license="Apache Software License 2.0", + author="vinyldns", + author_email="vinyldns-core@googlegroups.com", classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Topic :: Internet :: Name Service (DNS)', - 'License :: OSI Approved :: Apache Software License', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Topic :: Internet :: Name Service (DNS)", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", ], - keywords=[ - 'dns', 'python', 'vinyldns', - ], - description='Python client library for VinylDNS', + keywords=["dns", "python", "vinyldns"], + description="Python client library for VinylDNS", long_description=long_description, long_description_content_type="text/markdown", install_requires=[ - 'boto>=2.48.0', - 'future>=0.17.1', - 'requests>=2.20.0', - 'python-dateutil>=2.7.5', - ], - tests_require=[ - 'responses==0.10.4', - 'pytest==3.10.1', + "boto3>=1.26.0", + "future>=0.17.1", + "requests>=2.20.0", + "python-dateutil>=2.7.5", ], ) diff --git a/src/vinyldns/boto_request_signer.py b/src/vinyldns/boto_request_signer.py index 674ce98..39ebbb6 100644 --- a/src/vinyldns/boto_request_signer.py +++ b/src/vinyldns/boto_request_signer.py @@ -11,107 +11,150 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""TODO: Add module docstring.""" + +from __future__ import annotations import logging from datetime import datetime -from hashlib import sha256 +from typing import Dict, Optional, Union +import urllib.parse as urlparse -import requests.compat as urlparse -from boto.dynamodb2.layer1 import DynamoDBConnection +from botocore.auth import SigV4Auth +from botocore.awsrequest import AWSRequest +from botocore.credentials import Credentials logger = logging.getLogger(__name__) -__all__ = [u'BotoRequestSigner'] +__all__ = ["BotoRequestSigner"] -class BotoRequestSigner(object): - """TODO: Add class docstring.""" +class BotoRequestSigner: + """ + Signs HTTP requests using AWS Signature Version 4 for the VinylDNS service. + """ - def __init__(self, index_url, access_key, secret_access_key): - """TODO: Add method docstring.""" + def __init__( + self, + index_url: str, + access_key: str, + secret_access_key: str, + ) -> None: url = urlparse.urlparse(index_url) - self.boto_connection = DynamoDBConnection( - host=url.hostname, - port=url.port, - aws_access_key_id=access_key, - aws_secret_access_key=secret_access_key, - is_secure=False) + scheme = url.scheme or "https" + host = url.hostname + port = url.port + + if host is None: + raise ValueError(f"Invalid index_url (missing host): {index_url}") + + self.netloc = f"{host}: {port}" if port else host + self.base_url = f"{scheme}: //{self.netloc}" + self.region_name = "us-east-1" + self.service_name = "VinylDNS" + self.credentials = Credentials(access_key, secret_access_key) @staticmethod - def __canonical_date(headers): + def __canonical_date(headers: Dict[str, str]) -> str: """ - Derive canonical date (ISO 8601 string). + Resolve an ISO8601-like date from headers, falling back to current UTC. - Either from headers (if possible) or synthesize it if no usable header exists. + Checks 'X-Amz-Date' (ISO8601 basic) and 'Date' (HTTP-date), + and returns an ISO8601 basic formatted string. """ - iso_format = u'%Y%m%dT%H%M%SZ' - http_format = u'%a, %d %b %Y %H:%M:%S GMT' + iso_format = "%Y%m%dT%H%M%SZ" + http_format = "%a, %d %b %Y %H:%M:%S GMT" - def try_parse(date_string, format): + def try_parse( + date_string: Optional[str], + fmt: str, + ) -> Optional[datetime]: if date_string is None: return None try: - return datetime.strptime(date_string, format) + return datetime.strptime(date_string, fmt) except ValueError: return None - amz_date = try_parse(headers.get(u'X-Amz-Date'), iso_format) - http_date = try_parse(headers.get(u'Date'), http_format) + amz_date = try_parse(headers.get("X-Amz-Date"), iso_format) + http_date = try_parse(headers.get("Date"), http_format) fallback_date = datetime.utcnow() - date = next(d for d in [amz_date, http_date, fallback_date] if d is not None) + date = next( + d for d in (amz_date, http_date, fallback_date) if d is not None + ) return date.strftime(iso_format) - def build_auth_header(self, method, path, headers, body, params=None): - """Construct an Authorization header, using boto.""" - request = self.boto_connection.build_base_http_request( - method=method, - path=path, - auth_path=path, - headers=headers, - data=body, - params=params or {}) + def build_auth_header( + self, + method: str, + path: str, + headers: Optional[Dict[str, str]], + body: Optional[Union[str, bytes]], + params: Optional[Dict[str, Union[str, bytes]]] = None, + ) -> str: + """ + Build the AWS SigV4 Authorization header for the given request parameters. + """ + hdrs: Dict[str, str] = dict(headers or {}) + hdrs.setdefault("Host", self.netloc) - auth_handler = self.boto_connection._auth_handler + # Normalize body to bytes + if body is None: + data = b"" + elif isinstance(body, str): + data = body.encode("utf-8") + else: + data = body - timestamp = BotoRequestSigner.__canonical_date(headers) - request.timestamp = timestamp[0:8] + query = generate_canonical_query_string(params or {}) - request.region_name = u'us-east-1' - request.service_name = u'VinylDNS' + if not path.startswith("/"): + path = "/" + path - credential_scope = u'/'.join([request.timestamp, request.region_name, request.service_name, u'aws4_request']) + url = f"{self.base_url}{path}" + if query: + url = f"{url}?{query}" - canonical_request = auth_handler.canonical_request(request) - split_request = canonical_request.split('\n') + aws_request = AWSRequest( + method=method, + url=url, + data=data, + headers=hdrs, + ) - if params != {} and split_request[2] == '': - split_request[2] = generate_canonical_query_string(params) - canonical_request = '\n'.join(split_request) + SigV4Auth( + self.credentials, + self.service_name, + self.region_name, + ).add_auth(aws_request) - hashed_request = sha256(canonical_request.encode(u'utf-8')).hexdigest() + return aws_request.headers["Authorization"] - string_to_sign = u'\n'.join([u'AWS4-HMAC-SHA256', timestamp, credential_scope, hashed_request]) - signature = auth_handler.signature(request, string_to_sign) - headers_to_sign = auth_handler.headers_to_sign(request) - auth_header = u','.join([ - u'AWS4-HMAC-SHA256 Credential=%s' % auth_handler.scope(request), - u'SignedHeaders=%s' % auth_handler.signed_headers(headers_to_sign), - u'Signature=%s' % signature]) +def generate_canonical_query_string( + params: Dict[str, Union[str, bytes]], +) -> str: + """ + Generate a canonical (sorted + percent-encoded) query string suitable for SigV4. + """ + if not params: + return "" - return auth_header + def _to_str(value: Union[str, bytes]) -> str: + if isinstance(value, bytes): + return value.decode("utf-8") + return str(value) + encoded_pairs = [] -def generate_canonical_query_string(params): - """ - Using in place of canonical_query_string from boto/auth.py to support POST requests with query parameters - """ - post_params = [] for param in sorted(params): - value = params[param].encode('utf-8') - import urllib - post_params.append('%s=%s' % (urllib.parse.quote(param, safe='-_.~'), - urllib.parse.quote(value, safe='-_.~'))) - return '&'.join(post_params) + value = _to_str(params[param]) + encoded_pairs.append( + "%s=%s" + % ( + urlparse.quote(param, safe="-_.~"), + urlparse.quote(value, safe="-_.~"), + ) + ) + + return "&".join(encoded_pairs) diff --git a/tests/test_batch_change.py b/tests/test_batch_change.py index 5f5a325..d0f24da 100644 --- a/tests/test_batch_change.py +++ b/tests/test_batch_change.py @@ -1,7 +1,9 @@ # Copyright 2018 Comcast Cable Communications Management, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. +# +# Licensed under the License, Version 2.0 +# You may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -44,8 +46,8 @@ def check_single_changes_are_same(a, b): assert a.ttl == b.ttl assert a.record_data == b.record_data if a.validation_errors: - for l, r in zip(a.validation_errors, b.validation_errors): - check_validation_errors_are_same(l, r) + for left_error, right_error in zip(a.validation_errors, b.validation_errors): + check_validation_errors_are_same(left_error, right_error) def check_batch_changes_are_same(a, b): @@ -63,8 +65,8 @@ def check_batch_changes_are_same(a, b): assert a.review_comment == b.review_comment assert a.review_timestamp == b.review_timestamp assert a.scheduled_time == b.scheduled_time - for l, r in zip(a.changes, b.changes): - check_single_changes_are_same(l, r) + for left_change, right_change in zip(a.changes, b.changes): + check_single_changes_are_same(left_change, right_change) def test_create_batch_change(mocked_responses, vinyldns_client): @@ -162,8 +164,8 @@ def test_approve_batch_change(mocked_responses, vinyldns_client): def test_reject_batch_change(mocked_responses, vinyldns_client): - error_message = "Zone Discovery Failed: zone for \"foo.bar.com\" does not exist in VinylDNS. \ - If zone exists, then it must be connected to in VinylDNS." + error_message = "Zone Discovery Failed: zone for \"foo.bar.com\" does not exist in VinylDNS. " \ + "If zone exists, then it must be connected to in VinylDNS." error = ValidationError('ZoneDiscoveryError', error_message) @@ -243,12 +245,12 @@ def test_list_batch_change_summaries(mocked_responses, vinyldns_client): assert r.max_items == lbcs.max_items assert r.ignore_access == lbcs.ignore_access assert r.approval_status == lbcs.approval_status - for l, r in zip(r.batch_changes, lbcs.batch_changes): - assert l.user_id == r.user_id - assert l.user_name == r.user_name - assert l.comments == r.comments - assert l.created_timestamp == r.created_timestamp - assert l.total_changes == r.total_changes - assert l.status == r.status - assert l.id == r.id - assert l.owner_group_id == r.owner_group_id + for left_bc, right_bc in zip(r.batch_changes, lbcs.batch_changes): + assert left_bc.user_id == right_bc.user_id + assert left_bc.user_name == right_bc.user_name + assert left_bc.comments == right_bc.comments + assert left_bc.created_timestamp == right_bc.created_timestamp + assert left_bc.total_changes == right_bc.total_changes + assert left_bc.status == right_bc.status + assert left_bc.id == right_bc.id + assert left_bc.owner_group_id == right_bc.owner_group_id diff --git a/tests/test_membership.py b/tests/test_membership.py index 0950546..530f835 100644 --- a/tests/test_membership.py +++ b/tests/test_membership.py @@ -1,7 +1,9 @@ # Copyright 2018 Comcast Cable Communications Management, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. +# +# Licensed under the License, Version 2.0 +# You may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -31,8 +33,10 @@ def check_groups_are_same(a, b): assert a.created == b.created assert a.name == b.name assert a.email == b.email - assert all([l.__dict__ == r.__dict__ for l, r in zip(a.members, b.members)]) - assert all([l.__dict__ == r.__dict__ for l, r in zip(a.admins, b.admins)]) + for left_member, right_member in zip(a.members, b.members): + assert left_member.__dict__ == right_member.__dict__ + for left_admin, right_admin in zip(a.admins, b.admins): + assert left_admin.__dict__ == right_admin.__dict__ def test_create_group(mocked_responses, vinyldns_client): @@ -45,7 +49,7 @@ def test_create_group(mocked_responses, vinyldns_client): def test_update_group(mocked_responses, vinyldns_client): mocked_responses.add( - responses.PUT, 'http://test.com/groups/' + sample_group.id, + responses.PUT, f'http://test.com/groups/{sample_group.id}', body=to_json_string(sample_group), status=200) r = vinyldns_client.update_group(sample_group) check_groups_are_same(sample_group, r) @@ -77,94 +81,94 @@ def test_list_my_groups(mocked_responses, vinyldns_client): assert sample_list_groups.group_name_filter == r.group_name_filter assert sample_list_groups.next_id == r.next_id - for l, r in zip(sample_list_groups.groups, r.groups): - check_groups_are_same(l, r) + for left_group, right_group in zip(sample_list_groups.groups, r.groups): + check_groups_are_same(left_group, right_group) def test_list_all_my_groups(mocked_responses, vinyldns_client): sample_list_groups1 = ListGroupsResponse([sample_group], 1, '*', 'start-from', 'next-id') - - # Set next id to None to indicate end of list sample_list_groups2 = ListGroupsResponse([sample_group2], 1, '*', 'start-from', next_id=None) mocked_responses.add( responses.GET, 'http://test.com/groups?groupNameFilter=*', body=to_json_string(sample_list_groups1), status=200) - - # list all puts the next id from the first response into the startFrom for the next request mocked_responses.add( responses.GET, 'http://test.com/groups?groupNameFilter=*&startFrom=next-id', body=to_json_string(sample_list_groups2), status=200) - r = vinyldns_client.list_all_my_groups('*') + r = vinyldns_client.list_all_my_groups('*') assert r.start_from is None assert r.next_id is None assert sample_list_groups1.group_name_filter == r.group_name_filter - for l, r in zip([sample_group, sample_group2], r.groups): - check_groups_are_same(l, r) + for left_group, right_group in zip([sample_group, sample_group2], r.groups): + check_groups_are_same(left_group, right_group) def test_list_members(mocked_responses, vinyldns_client): member1 = Member('some-id', 'user-name', 'first', 'last', 'test@test.com', datetime.datetime.utcnow(), False) member2 = Member('some-id2', 'user-name2', 'first2', 'last2', 'test2@test.com', datetime.datetime.utcnow(), False) - lmr = ListMembersResponse([member1, member2], start_from='start', next_id='next', max_items=100) + list_members_response = ListMembersResponse([member1, member2], start_from='start', next_id='next', max_items=100) mocked_responses.add( responses.GET, 'http://test.com/groups/foo/members?startFrom=start&maxItems=100', - body=to_json_string(lmr), status=200 + body=to_json_string(list_members_response), status=200 ) + r = vinyldns_client.list_members_group('foo', 'start', 100) - r.start_from = lmr.start_from - r.next_id = lmr.next_id - r.max_items = lmr.max_items + r.start_from = list_members_response.start_from + r.next_id = list_members_response.next_id + r.max_items = list_members_response.max_items - for l, r in zip(lmr.members, r.members): - assert l.id == r.id - assert l.user_name == r.user_name - assert l.first_name == r.first_name - assert l.last_name == r.last_name - assert l.email == r.email - assert l.created == r.created - assert l.is_admin == r.is_admin + for left_member, right_member in zip(list_members_response.members, r.members): + assert left_member.id == right_member.id + assert left_member.user_name == right_member.user_name + assert left_member.first_name == right_member.first_name + assert left_member.last_name == right_member.last_name + assert left_member.email == right_member.email + assert left_member.created == right_member.created + assert left_member.is_admin == right_member.is_admin def test_list_group_admins(mocked_responses, vinyldns_client): user1 = User('id', 'test200', 'Bobby', 'Bonilla', 'bob@bob.com', datetime.datetime.utcnow()) user2 = User('id2', 'test2002', 'Frank', 'Bonilla', 'frank@bob.com', datetime.datetime.utcnow()) - lar = ListAdminsResponse([user1, user2]) + list_admins_response = ListAdminsResponse([user1, user2]) mocked_responses.add( responses.GET, 'http://test.com/groups/foo/admins', - body=to_json_string(lar), status=200 + body=to_json_string(list_admins_response), status=200 ) + r = vinyldns_client.list_group_admins('foo') - for l, r in zip(lar.admins, r.admins): - assert l.id == r.id - assert l.user_name == r.user_name - assert l.first_name == r.first_name - assert l.last_name == r.last_name - assert l.email == r.email - assert l.created == r.created - assert l.lock_status == r.lock_status + for left_user, right_user in zip(list_admins_response.admins, r.admins): + assert left_user.id == right_user.id + assert left_user.user_name == right_user.user_name + assert left_user.first_name == right_user.first_name + assert left_user.last_name == right_user.last_name + assert left_user.email == right_user.email + assert left_user.created == right_user.created + assert left_user.lock_status == right_user.lock_status def test_list_group_changes(mocked_responses, vinyldns_client): change1 = GroupChange(sample_group, 'Create', 'user', None, 'id', datetime.datetime.utcnow()) change2 = GroupChange(sample_group2, 'Update', 'user', sample_group, 'id2', datetime.datetime.utcnow()) - lgcr = ListGroupChangesResponse([change1, change2], 'start', 'next', 100) + list_group_changes_response = ListGroupChangesResponse([change1, change2], 'start', 'next', 100) mocked_responses.add( responses.GET, 'http://test.com/groups/foo/activity?startFrom=start&maxItems=100', - body=to_json_string(lgcr), status=200 + body=to_json_string(list_group_changes_response), status=200 ) + r = vinyldns_client.list_group_changes('foo', 'start', 100) - assert r.next_id == lgcr.next_id - assert r.start_from == lgcr.start_from - assert r.max_items == lgcr.max_items - for l, r in zip(lgcr.changes, r.changes): - assert l.change_type == r.change_type - assert l.user_id == r.user_id - assert l.id == r.id - assert l.created == r.created - check_groups_are_same(l.new_group, r.new_group) - check_groups_are_same(l.old_group, r.old_group) + assert r.next_id == list_group_changes_response.next_id + assert r.start_from == list_group_changes_response.start_from + assert r.max_items == list_group_changes_response.max_items + + for left_change, right_change in zip(list_group_changes_response.changes, r.changes): + assert left_change.change_type == right_change.change_type + assert left_change.user_id == right_change.user_id + assert left_change.id == right_change.id + assert left_change.created == right_change.created + check_groups_are_same(left_change.new_group, right_change.new_group) + check_groups_are_same(left_change.old_group, right_change.old_group) def test_group_serdes(): diff --git a/tests/test_records.py b/tests/test_records.py index 5412c6f..c512843 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,7 +1,9 @@ # Copyright 2018 Comcast Cable Communications Management, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. +# +# Licensed under the License, Version 2.0 +# You may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -29,7 +31,8 @@ def check_record_sets_are_equal(a, b): assert a.ttl == b.ttl assert a.name == b.name assert a.owner_group_id == b.owner_group_id - assert all([l.__dict__ == r.__dict__ for l, r in zip(a.records, b.records)]) + for left_record, right_record in zip(a.records, b.records): + assert left_record.__dict__ == right_record.__dict__ def check_record_set_changes_are_equal(a, b): @@ -51,7 +54,7 @@ def check_record_set_changes_are_equal(a, b): def test_create_record_set(record_set, mocked_responses, vinyldns_client): change = gen_rs_change(record_set) mocked_responses.add( - responses.POST, 'http://test.com/zones/{0}/recordsets'.format(record_set.zone_id), + responses.POST, f'http://test.com/zones/{record_set.zone_id}/recordsets', body=to_json_string(change), status=200 ) r = vinyldns_client.create_record_set(record_set) @@ -64,7 +67,7 @@ def test_update_record_set(record_set, mocked_responses, vinyldns_client): rs.id = rs.name + 'id' change = gen_rs_change(rs) mocked_responses.add( - responses.PUT, 'http://test.com/zones/{0}/recordsets/{1}'.format(rs.zone_id, rs.id), + responses.PUT, f'http://test.com/zones/{rs.zone_id}/recordsets/{rs.id}', body=to_json_string(change), status=200 ) r = vinyldns_client.update_record_set(rs) @@ -77,7 +80,7 @@ def test_delete_record_set(record_set, mocked_responses, vinyldns_client): rs.id = rs.name + 'id' change = gen_rs_change(rs) mocked_responses.add( - responses.DELETE, 'http://test.com/zones/{0}/recordsets/{1}'.format(rs.zone_id, rs.id), + responses.DELETE, f'http://test.com/zones/{rs.zone_id}/recordsets/{rs.id}', body=to_json_string(change), status=200 ) r = vinyldns_client.delete_record_set(rs.zone_id, rs.id) @@ -90,7 +93,7 @@ def test_get_record_set(record_set, mocked_responses, vinyldns_client): rs.id = rs.name + 'id' response = {'recordSet': rs} mocked_responses.add( - responses.GET, 'http://test.com/zones/{0}/recordsets/{1}'.format(rs.zone_id, rs.id), + responses.GET, f'http://test.com/zones/{rs.zone_id}/recordsets/{rs.id}', body=to_json_string(response), status=200 ) r = vinyldns_client.get_record_set(rs.zone_id, rs.id) @@ -99,47 +102,47 @@ def test_get_record_set(record_set, mocked_responses, vinyldns_client): def test_list_record_sets(mocked_responses, vinyldns_client): - lrr = ListRecordSetsResponse(record_set_values, 'start', 'next', 100, '*') + list_response = ListRecordSetsResponse(record_set_values, 'start', 'next', 100, '*') mocked_responses.add( responses.GET, - 'http://test.com/zones/{0}/recordsets?startFrom=start&maxItems=100&recordNameFilter=*'.format(forward_zone.id), - body=to_json_string(lrr), status=200 + f'http://test.com/zones/{forward_zone.id}/recordsets?startFrom=start&maxItems=100&recordNameFilter=*', + body=to_json_string(list_response), status=200 ) r = vinyldns_client.list_record_sets(forward_zone.id, 'start', 100, '*') - assert r.start_from == lrr.start_from - assert r.next_id == lrr.next_id - assert r.record_name_filter == lrr.record_name_filter - assert r.max_items == lrr.max_items - for l, r in zip(r.record_sets, lrr.record_sets): - check_record_sets_are_equal(l, r) + assert r.start_from == list_response.start_from + assert r.next_id == list_response.next_id + assert r.record_name_filter == list_response.record_name_filter + assert r.max_items == list_response.max_items + for left_rs, right_rs in zip(r.record_sets, list_response.record_sets): + check_record_sets_are_equal(left_rs, right_rs) def test_search_record_sets(mocked_responses, vinyldns_client): - lrr = ListRecordSetsResponse(record_set_values, 'start', 'next', 100, '*') + list_response = ListRecordSetsResponse(record_set_values, 'start', 'next', 100, '*') all_record_types = list(record_sets.keys()) record_type_filter = '' for record_type in all_record_types: - record_type_filter += '&recordTypeFilter[]={0}'.format(record_type) + record_type_filter += f'&recordTypeFilter[]={record_type}' mocked_responses.add( responses.GET, - 'http://test.com/recordsets?startFrom=start&maxItems=100&recordNameFilter=*' + - record_type_filter + '&recordOwnerGroupFilter=owner-group-id&nameSort=DESC', - body=to_json_string(lrr), status=200 + f'http://test.com/recordsets?startFrom=start&maxItems=100&recordNameFilter=*{record_type_filter}' + '&recordOwnerGroupFilter=owner-group-id&nameSort=DESC', + body=to_json_string(list_response), status=200 ) r = vinyldns_client.search_record_sets('start', 100, '*', all_record_types, 'owner-group-id', 'DESC') - assert r.start_from == lrr.start_from - assert r.next_id == lrr.next_id - assert r.record_name_filter == lrr.record_name_filter - assert r.max_items == lrr.max_items - for l, r in zip(r.record_sets, lrr.record_sets): - check_record_sets_are_equal(l, r) + assert r.start_from == list_response.start_from + assert r.next_id == list_response.next_id + assert r.record_name_filter == list_response.record_name_filter + assert r.max_items == list_response.max_items + for left_rs, right_rs in zip(r.record_sets, list_response.record_sets): + check_record_sets_are_equal(left_rs, right_rs) def test_get_record_set_change(record_set, mocked_responses, vinyldns_client): change = gen_rs_change(record_set) mocked_responses.add( responses.GET, - 'http://test.com/zones/{0}/recordsets/{1}/changes/{2}'.format(record_set.zone_id, record_set.id, change.id), + f'http://test.com/zones/{record_set.zone_id}/recordsets/{record_set.id}/changes/{change.id}', body=to_json_string(change), status=200 ) r = vinyldns_client.get_record_set_change(record_set.zone_id, record_set.id, change.id) @@ -148,17 +151,19 @@ def test_get_record_set_change(record_set, mocked_responses, vinyldns_client): def test_list_record_set_changes(mocked_responses, vinyldns_client): changes = [gen_rs_change(c) for c in record_set_values] - lrscr = ListRecordSetChangesResponse(forward_zone.id, changes, 'next', 'start', 100) - mocked_responses.add(responses.GET, - 'http://test.com/zones/{0}/recordsetchanges?startFrom=start&maxItems=100'.format( - forward_zone.id), body=to_json_string(lrscr), status=200) + list_changes_response = ListRecordSetChangesResponse(forward_zone.id, changes, 'next', 'start', 100) + mocked_responses.add( + responses.GET, + f'http://test.com/zones/{forward_zone.id}/recordsetchanges?startFrom=start&maxItems=100', + body=to_json_string(list_changes_response), status=200 + ) r = vinyldns_client.list_record_set_changes(forward_zone.id, 'start', 100) - r.start_from = lrscr.start_from - r.next_id = lrscr.next_id - r.max_items = lrscr.max_items - r.zone_id = lrscr.zone_id - for l, r in zip(r.record_set_changes, lrscr.record_set_changes): - check_record_set_changes_are_equal(l, r) + r.start_from = list_changes_response.start_from + r.next_id = list_changes_response.next_id + r.max_items = list_changes_response.max_items + r.zone_id = list_changes_response.zone_id + for left_change, right_change in zip(r.record_set_changes, list_changes_response.record_set_changes): + check_record_set_changes_are_equal(left_change, right_change) def test_record_set_serdes(record_set): @@ -169,33 +174,34 @@ def test_record_set_serdes(record_set): def test_record_set_changes_serdes(record_set): - a = gen_rs_change(record_set) - s = to_json_string(a) - b = from_json_string(s, RecordSetChange.from_dict) - - assert a.user_id == b.user_id - assert a.change_type == b.change_type - assert a.status == b.status - assert a.created == b.created - assert a.system_message == b.system_message - assert a.id == b.id - assert a.user_name == b.user_name - assert a.zone.id == b.zone.id - check_record_sets_are_equal(a.record_set, b.record_set) - check_record_sets_are_equal(a.updates, b.updates) + change = gen_rs_change(record_set) + s = to_json_string(change) + parsed_change = from_json_string(s, RecordSetChange.from_dict) + + assert change.user_id == parsed_change.user_id + assert change.change_type == parsed_change.change_type + assert change.status == parsed_change.status + assert change.created == parsed_change.created + assert change.system_message == parsed_change.system_message + assert change.id == parsed_change.id + assert change.user_name == parsed_change.user_name + assert change.zone.id == parsed_change.zone.id + check_record_sets_are_equal(change.record_set, parsed_change.record_set) + check_record_sets_are_equal(change.updates, parsed_change.updates) def test_list_record_set_response_serdes(): - a = ListRecordSetsResponse(record_sets=record_set_values, start_from='some-start', next_id='next', - max_items=100, - record_name_filter='foo*') - s = to_json_string(a) - b = from_json_string(s, ListRecordSetsResponse.from_dict) - - assert a.start_from == b.start_from - assert a.next_id == b.next_id - assert a.max_items == b.max_items - assert a.record_name_filter == b.record_name_filter - assert len(a.record_sets) == len(b.record_sets) - for l, r in zip(a.record_sets, b.record_sets): - check_record_sets_are_equal(l, r) + response = ListRecordSetsResponse( + record_sets=record_set_values, start_from='some-start', next_id='next', + max_items=100, record_name_filter='foo*' + ) + s = to_json_string(response) + parsed_response = from_json_string(s, ListRecordSetsResponse.from_dict) + + assert response.start_from == parsed_response.start_from + assert response.next_id == parsed_response.next_id + assert response.max_items == parsed_response.max_items + assert response.record_name_filter == parsed_response.record_name_filter + assert len(response.record_sets) == len(parsed_response.record_sets) + for left_rs, right_rs in zip(response.record_sets, parsed_response.record_sets): + check_record_sets_are_equal(left_rs, right_rs) diff --git a/tests/test_zones.py b/tests/test_zones.py index 3682b81..7529e99 100644 --- a/tests/test_zones.py +++ b/tests/test_zones.py @@ -1,7 +1,9 @@ # Copyright 2018 Comcast Cable Communications Management, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. +# +# Licensed under the License, Version 2.0 +# You may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -45,7 +47,8 @@ def check_zones_are_same(a, b): assert a.backend_id == b.backend_id check_zone_connections_are_same(a.connection, b.connection) check_zone_connections_are_same(a.transfer_connection, b.transfer_connection) - assert all([l.__dict__ == r.__dict__ for l, r in zip(a.acl.rules, b.acl.rules)]) + for left_rule, right_rule in zip(a.acl.rules, b.acl.rules): + assert left_rule.__dict__ == right_rule.__dict__ def test_zone_serdes(): @@ -55,7 +58,8 @@ def test_zone_serdes(): assert z.name == forward_zone.name assert z.connection.primary_server == forward_zone.connection.primary_server - assert all([a.__dict__ == b.__dict__ for a, b in zip(z.acl.rules, forward_zone.acl.rules)]) + for left_rule, right_rule in zip(z.acl.rules, forward_zone.acl.rules): + assert left_rule.__dict__ == right_rule.__dict__ def test_connect_zone(mocked_responses, vinyldns_client): @@ -68,7 +72,7 @@ def test_connect_zone(mocked_responses, vinyldns_client): def test_update_zone(mocked_responses, vinyldns_client): mocked_responses.add( - responses.PUT, 'http://test.com/zones/{0}'.format(forward_zone.id), + responses.PUT, f'http://test.com/zones/{forward_zone.id}', body=to_json_string(sample_zone_change), status=200) r = vinyldns_client.update_zone(forward_zone) check_zones_are_same(forward_zone, r.zone) @@ -76,7 +80,7 @@ def test_update_zone(mocked_responses, vinyldns_client): def test_abandon_zone(mocked_responses, vinyldns_client): mocked_responses.add( - responses.DELETE, 'http://test.com/zones/{0}'.format(forward_zone.id), + responses.DELETE, f'http://test.com/zones/{forward_zone.id}', body=to_json_string(sample_zone_change), status=200) r = vinyldns_client.abandon_zone(forward_zone.id) check_zones_are_same(forward_zone, r.zone) @@ -84,7 +88,7 @@ def test_abandon_zone(mocked_responses, vinyldns_client): def test_sync_zone(mocked_responses, vinyldns_client): mocked_responses.add( - responses.POST, 'http://test.com/zones/{0}/sync'.format(forward_zone.id), + responses.POST, f'http://test.com/zones/{forward_zone.id}/sync', body=to_json_string(sample_zone_change), status=200) r = vinyldns_client.sync_zone(forward_zone.id) assert sample_zone_change.id == r.id @@ -96,8 +100,13 @@ def test_sync_zone(mocked_responses, vinyldns_client): def test_list_zones(mocked_responses, vinyldns_client): - lzr = ListZonesResponse(zones=[forward_zone, ip4_zone, ip6_zone], name_filter='*', start_from='start-from', - next_id='next', max_items=100) + lzr = ListZonesResponse( + zones=[forward_zone, ip4_zone, ip6_zone], + name_filter='*', + start_from='start-from', + next_id='next', + max_items=100 + ) mocked_responses.add( responses.GET, 'http://test.com/zones?nameFilter=*&startFrom=start-from&maxItems=100', body=to_json_string(lzr), status=200 @@ -107,13 +116,13 @@ def test_list_zones(mocked_responses, vinyldns_client): assert r.start_from == lzr.start_from assert r.next_id == lzr.next_id assert r.max_items == lzr.max_items - for l, r in zip(lzr.zones, r.zones): - check_zones_are_same(l, r) + for left_zone, right_zone in zip(lzr.zones, r.zones): + check_zones_are_same(left_zone, right_zone) def test_get_zone(mocked_responses, vinyldns_client): mocked_responses.add( - responses.GET, 'http://test.com/zones/{0}'.format(forward_zone.id), + responses.GET, f'http://test.com/zones/{forward_zone.id}', body=to_json_string({'zone': forward_zone}), status=200) r = vinyldns_client.get_zone(forward_zone.id) check_zones_are_same(forward_zone, r) @@ -121,7 +130,7 @@ def test_get_zone(mocked_responses, vinyldns_client): def test_get_zone_by_name(mocked_responses, vinyldns_client): mocked_responses.add( - responses.GET, 'http://test.com/zones/name/{0}'.format(forward_zone.name), + responses.GET, f'http://test.com/zones/name/{forward_zone.name}', body=to_json_string({'zone': forward_zone}), status=200) r = vinyldns_client.get_zone_by_name(forward_zone.name) check_zones_are_same(forward_zone, r) @@ -134,26 +143,26 @@ def test_list_zone_changes(mocked_responses, vinyldns_client): created=datetime.utcnow(), system_message='msg', id='zone-change-id2') lzcr = ListZoneChangesResponse(forward_zone.id, [change1, change2], 'next', 'start', 100) mocked_responses.add( - responses.GET, 'http://test.com/zones/{0}/changes?startFrom=start&maxItems=100'.format(forward_zone.id), + responses.GET, f'http://test.com/zones/{forward_zone.id}/changes?startFrom=start&maxItems=100', body=to_json_string(lzcr), status=200 ) r = vinyldns_client.list_zone_changes(forward_zone.id, 'start', 100) assert r.start_from == lzcr.start_from assert r.next_id == lzcr.next_id assert r.max_items == lzcr.max_items - for l, r in zip(lzcr.zone_changes, r.zone_changes): - assert l.id == r.id - assert l.user_id == r.user_id - assert l.change_type == r.change_type - assert l.status == r.status - assert l.created == r.created - assert l.system_message == r.system_message - check_zones_are_same(l.zone, r.zone) + for left_change, right_change in zip(lzcr.zone_changes, r.zone_changes): + assert left_change.id == right_change.id + assert left_change.user_id == right_change.user_id + assert left_change.change_type == right_change.change_type + assert left_change.status == right_change.status + assert left_change.created == right_change.created + assert left_change.system_message == right_change.system_message + check_zones_are_same(left_change.zone, right_change.zone) def test_add_acl_rule(mocked_responses, vinyldns_client): mocked_responses.add( - responses.PUT, 'http://test.com/zones/{0}/acl/rules'.format(forward_zone.id), + responses.PUT, f'http://test.com/zones/{forward_zone.id}/acl/rules', body=to_json_string(sample_zone_change) ) r = vinyldns_client.add_zone_acl_rule(forward_zone.id, acl_rule) @@ -162,7 +171,7 @@ def test_add_acl_rule(mocked_responses, vinyldns_client): def test_delete_acl_rule(mocked_responses, vinyldns_client): mocked_responses.add( - responses.DELETE, 'http://test.com/zones/{0}/acl/rules'.format(forward_zone.id), + responses.DELETE, f'http://test.com/zones/{forward_zone.id}/acl/rules', body=to_json_string(sample_zone_change) ) r = vinyldns_client.delete_zone_acl_rule(forward_zone.id, acl_rule) diff --git a/tox.ini b/tox.ini index 09bcdff..21ecd04 100644 --- a/tox.ini +++ b/tox.ini @@ -1,65 +1,49 @@ -; a generative tox configuration, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist - +; tox configuration for VinylDNS Python project [tox] envlist = clean, check, - {py27,py34,py35,py36,py37}, + py3, report, func_test [testenv] -basepython = - py27: {env:TOXPYTHON:python2.7} - py34: {env:TOXPYTHON:python3.4} - py35: {env:TOXPYTHON:python3.5} - py36: {env:TOXPYTHON:python3.6} - py37: {env:TOXPYTHON:python3.7} - {clean,check,report,codecov,func_test}: {env:TOXPYTHON:python3} +basepython = python3 +usedevelop = true + setenv = - PYTHONPATH={toxinidir}/tests + PYTHONPATH={toxinidir}/src PYTHONUNBUFFERED=yes -passenv = - * -usedevelop = true + deps = - pytest - pytest-travis-fold - pytest-cov + pytest>=7,<9 + pytest-cov>=4 responses python-dateutil + boto3>=1.26.0 commands = - {posargs:pytest --cov --cov-report=term-missing -vv tests} + pytest --cov=src --cov-report=term-missing -vv tests [testenv:check] deps = - docutils - flake8==3.7.9 - + flake8>=6.1,<8 skip_install = true commands = flake8 src tests setup.py -[testenv:codecov] -deps = - codecov -skip_install = true -commands = - coverage xml --ignore-errors - codecov [] - [testenv:report] -deps = coverage +deps = coverage>=7.3 skip_install = true commands = coverage report coverage html [testenv:clean] -commands = coverage erase +deps = coverage>=7.3 skip_install = true -deps = coverage +commands = + coverage erase [testenv:func_test] commands_pre = @@ -67,11 +51,6 @@ commands_pre = commands_post = bash -c ./docker/remove-vinyl-containers.sh commands = - {posargs:pytest -vv func_tests} -whitelist_externals = + pytest -vv func_tests +allowlist_externals = bash - -[travis] -python = - 2.7: check, py27 - 3.6: check, py36, func_test From a41c78462a1d13905eff1182be976dc4bd3fef4a Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Sun, 21 Dec 2025 19:38:44 +0530 Subject: [PATCH 2/7] update Signed-off-by: Jay07GIT --- tests/test_batch_change.py | 6 ++---- tests/test_membership.py | 6 ++---- tests/test_records.py | 6 ++---- tests/test_zones.py | 5 +---- 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/tests/test_batch_change.py b/tests/test_batch_change.py index d0f24da..43f2ef0 100644 --- a/tests/test_batch_change.py +++ b/tests/test_batch_change.py @@ -1,9 +1,7 @@ # Copyright 2018 Comcast Cable Communications Management, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); -# -# Licensed under the License, Version 2.0 -# You may not use this file except in compliance with the License. +# you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -13,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""TODO: Add module docstring.""" + from datetime import datetime, timedelta from dateutil.tz import tzlocal diff --git a/tests/test_membership.py b/tests/test_membership.py index 530f835..e75d0a0 100644 --- a/tests/test_membership.py +++ b/tests/test_membership.py @@ -1,9 +1,7 @@ # Copyright 2018 Comcast Cable Communications Management, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); -# -# Licensed under the License, Version 2.0 -# You may not use this file except in compliance with the License. +# you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -13,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""TODO: Add module docstring.""" + import datetime import responses diff --git a/tests/test_records.py b/tests/test_records.py index c512843..a5c4f28 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,9 +1,7 @@ # Copyright 2018 Comcast Cable Communications Management, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); -# -# Licensed under the License, Version 2.0 -# You may not use this file except in compliance with the License. +# you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -13,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""TODO: Add module docstring.""" + import copy import responses diff --git a/tests/test_zones.py b/tests/test_zones.py index 7529e99..0519efa 100644 --- a/tests/test_zones.py +++ b/tests/test_zones.py @@ -1,9 +1,7 @@ # Copyright 2018 Comcast Cable Communications Management, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); -# -# Licensed under the License, Version 2.0 -# You may not use this file except in compliance with the License. +# you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -13,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""TODO: Add module docstring.""" import json from datetime import datetime From 49d4c954d208b31b8c7b128aea4466d5620b0e9a Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Sun, 21 Dec 2025 20:38:08 +0530 Subject: [PATCH 3/7] updated the deprecated code Signed-off-by: Jay07GIT --- .github/workflows/verify.yml | 34 +++++++++++++++++++++++++++++ src/vinyldns/boto_request_signer.py | 4 ++-- src/vinyldns/client.py | 4 ++-- tests/sampledata.py | 12 +++++----- tests/test_batch_change.py | 20 ++++++++--------- tests/test_membership.py | 14 ++++++------ tests/test_zones.py | 6 ++--- tox.ini | 8 +++---- 8 files changed, 68 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/verify.yml diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 0000000..a166d4c --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,34 @@ +name: Verify & Code-Coverage + +on: + pull_request: + push: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + + - name: Run tests + run: tox -e check,py3 + + - name: Upload coverage to Codecov + if: success() + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true diff --git a/src/vinyldns/boto_request_signer.py b/src/vinyldns/boto_request_signer.py index 39ebbb6..dded002 100644 --- a/src/vinyldns/boto_request_signer.py +++ b/src/vinyldns/boto_request_signer.py @@ -15,7 +15,7 @@ from __future__ import annotations import logging -from datetime import datetime +from datetime import datetime, UTC from typing import Dict, Optional, Union import urllib.parse as urlparse @@ -77,7 +77,7 @@ def try_parse( amz_date = try_parse(headers.get("X-Amz-Date"), iso_format) http_date = try_parse(headers.get("Date"), http_format) - fallback_date = datetime.utcnow() + fallback_date = datetime.now(UTC) date = next( d for d in (amz_date, http_date, fallback_date) if d is not None diff --git a/src/vinyldns/client.py b/src/vinyldns/client.py index 88240af..66d4828 100644 --- a/src/vinyldns/client.py +++ b/src/vinyldns/client.py @@ -19,6 +19,7 @@ from builtins import str import requests +from datetime import datetime, UTC from future.moves.urllib.parse import parse_qs from future.utils import iteritems from requests.adapters import HTTPAdapter @@ -209,8 +210,7 @@ def __build_headers(new_headers, suppressed_keys): def canonical_header_name(field_name): return u'-'.join(word.capitalize() for word in field_name.split(u'-')) - import datetime - now = datetime.datetime.utcnow() + now = datetime.now(UTC) headers = {u'Content-Type': u'application/x-amz-json-1.0', u'Date': now.strftime(u'%a, %d %b %Y %H:%M:%S GMT'), u'X-Amz-Date': now.strftime(u'%Y%m%dT%H%M%SZ')} diff --git a/tests/sampledata.py b/tests/sampledata.py index 7f0720f..1146276 100644 --- a/tests/sampledata.py +++ b/tests/sampledata.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """TODO: Add module docstring.""" -from datetime import datetime +from datetime import datetime, UTC from vinyldns.record import RecordSet, RecordSetChange, AData, AAAAData, CNAMEData, PTRData, MXData, NSData, SOAData, \ SRVData, SPFData, SSHFPData, TXTData, RecordType @@ -30,7 +30,7 @@ connection=conn, transfer_connection=conn, acl=ZoneACL([acl_rule])) sample_zone_change = ZoneChange(zone=forward_zone, user_id='some-user', change_type='Create', status='Pending', - created=datetime.utcnow(), system_message=None, id='zone-change-id') + created=datetime.now(UTC), system_message=None, id='zone-change-id') record_sets = { RecordType.A: RecordSet(forward_zone.id, 'a-test', RecordType.A, 200, records=[AData('1.2.3.4')], @@ -61,14 +61,14 @@ record_set_values = record_sets.values() -sample_user = User('id', 'test200', 'Bobby', 'Bonilla', 'bob@bob.com', datetime.utcnow()) -sample_group = Group('ok', 'test@test.com', 'description', datetime.utcnow(), members=[sample_user], +sample_user = User('id', 'test200', 'Bobby', 'Bonilla', 'bob@bob.com', datetime.now(UTC)) +sample_group = Group('ok', 'test@test.com', 'description', datetime.now(UTC), members=[sample_user], admins=[sample_user], id='sample-group') -sample_group2 = Group('ok2', 'test@test.com', 'description', datetime.utcnow(), members=[sample_user], +sample_group2 = Group('ok2', 'test@test.com', 'description', datetime.now(UTC), members=[sample_user], admins=[sample_user], id='sample-group2') def gen_rs_change(record_set): return RecordSetChange(zone=forward_zone, record_set=record_set, user_id='test-user', - change_type='Create', status='Pending', created=datetime.utcnow(), system_message=None, + change_type='Create', status='Pending', created=datetime.now(UTC), system_message=None, updates=record_set, id='some-id', user_name='some-username') diff --git a/tests/test_batch_change.py b/tests/test_batch_change.py index 43f2ef0..43f7952 100644 --- a/tests/test_batch_change.py +++ b/tests/test_batch_change.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from datetime import datetime, timedelta +from datetime import datetime, timedelta, UTC from dateutil.tz import tzlocal import responses @@ -88,7 +88,7 @@ def test_create_batch_change(mocked_responses, vinyldns_client): except TypeError: tomorrow = datetime.now(tzlocal()).astimezone(tzlocal()) + timedelta(1) - bc = BatchChange('user-id', 'user-name', datetime.utcnow(), [arc, drc, drc_with_data], + bc = BatchChange('user-id', 'user-name', datetime.now(UTC), [arc, drc, drc_with_data], 'bcid', 'Scheduled', 'PendingReview', comments='batch change test', owner_group_id='owner-group-id', scheduled_time=tomorrow) @@ -122,7 +122,7 @@ def test_get_batch_change(mocked_responses, vinyldns_client): 'biz.bar.com', RecordType.A, 'Complete', 'id3', [], 'system-message', 'rchangeid3', 'rsid3', AData("5.6.7.8")) - bc = BatchChange('user-id', 'user-name', datetime.utcnow(), [arc, drc, drc_with_data], + bc = BatchChange('user-id', 'user-name', datetime.now(UTC), [arc, drc, drc_with_data], 'bcid', 'Complete', 'AutoApproved', comments='batch change test', owner_group_id='owner-group-id') @@ -145,11 +145,11 @@ def test_approve_batch_change(mocked_responses, vinyldns_client): 'baz.bar.com', RecordType.A, 'PendingReview', 'id2', [], 'system-message', 'rchangeid2', 'rsid2') - bc = BatchChange('user-id', 'user-name', datetime.utcnow(), [arc, drc], + bc = BatchChange('user-id', 'user-name', datetime.now(UTC), [arc, drc], 'bcid', 'Complete', 'ManuallyApproved', comments='batch change test', owner_group_id='owner-group-id', reviewer_id='admin-id', reviewer_user_name='admin', - review_comment='looks good', review_timestamp=datetime.utcnow()) + review_comment='looks good', review_timestamp=datetime.now(UTC)) mocked_responses.add( responses.POST, 'http://test.com/zones/batchrecordchanges/bcid/approve', @@ -175,11 +175,11 @@ def test_reject_batch_change(mocked_responses, vinyldns_client): 'reject2.bar.com', RecordType.A, 'Complete', 'id2', [], 'system-message', 'rchangeid2', 'rsid2') - bc = BatchChange('user-id', 'user-name', datetime.utcnow(), [arc, drc], + bc = BatchChange('user-id', 'user-name', datetime.now(UTC), [arc, drc], 'bcid', 'Rejected', 'Rejected', comments='batch change test', owner_group_id='owner-group-id', reviewer_id='admin-id', reviewer_user_name='admin', - review_comment='not good', review_timestamp=datetime.utcnow()) + review_comment='not good', review_timestamp=datetime.now(UTC)) mocked_responses.add( responses.POST, 'http://test.com/zones/batchrecordchanges/bcid/reject', @@ -212,7 +212,7 @@ def test_cancel_batch_change(mocked_responses, vinyldns_client): ) bc = BatchChange( - 'user-id', 'user-name', datetime.utcnow(), [arc, drc], 'bcid', + 'user-id', 'user-name', datetime.now(UTC), [arc, drc], 'bcid', 'Cancelled', 'Cancelled', owner_group_id='owner-group-id' ) @@ -227,10 +227,10 @@ def test_cancel_batch_change(mocked_responses, vinyldns_client): def test_list_batch_change_summaries(mocked_responses, vinyldns_client): - bcs1 = BatchChangeSummary('user-id', 'user-name', datetime.utcnow(), 10, 'id1', + bcs1 = BatchChangeSummary('user-id', 'user-name', datetime.now(UTC), 10, 'id1', 'Complete', 'AutoApproved', comments='comments', owner_group_id='owner-group-id') - bcs2 = BatchChangeSummary('user-id2', 'user-name2', datetime.utcnow(), 20, + bcs2 = BatchChangeSummary('user-id2', 'user-name2', datetime.now(UTC), 20, 'id2', 'Complete', 'AutoApproved', comments='comments2') lbcs = ListBatchChangeSummaries([bcs1, bcs2], 'start', 'next', 50) mocked_responses.add( diff --git a/tests/test_membership.py b/tests/test_membership.py index e75d0a0..46727a1 100644 --- a/tests/test_membership.py +++ b/tests/test_membership.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import datetime +from datetime import datetime, UTC import responses @@ -103,8 +103,8 @@ def test_list_all_my_groups(mocked_responses, vinyldns_client): def test_list_members(mocked_responses, vinyldns_client): - member1 = Member('some-id', 'user-name', 'first', 'last', 'test@test.com', datetime.datetime.utcnow(), False) - member2 = Member('some-id2', 'user-name2', 'first2', 'last2', 'test2@test.com', datetime.datetime.utcnow(), False) + member1 = Member('some-id', 'user-name', 'first', 'last', 'test@test.com', datetime.now(UTC), False) + member2 = Member('some-id2', 'user-name2', 'first2', 'last2', 'test2@test.com', datetime.now(UTC), False) list_members_response = ListMembersResponse([member1, member2], start_from='start', next_id='next', max_items=100) mocked_responses.add( responses.GET, 'http://test.com/groups/foo/members?startFrom=start&maxItems=100', @@ -127,8 +127,8 @@ def test_list_members(mocked_responses, vinyldns_client): def test_list_group_admins(mocked_responses, vinyldns_client): - user1 = User('id', 'test200', 'Bobby', 'Bonilla', 'bob@bob.com', datetime.datetime.utcnow()) - user2 = User('id2', 'test2002', 'Frank', 'Bonilla', 'frank@bob.com', datetime.datetime.utcnow()) + user1 = User('id', 'test200', 'Bobby', 'Bonilla', 'bob@bob.com', datetime.now(UTC)) + user2 = User('id2', 'test2002', 'Frank', 'Bonilla', 'frank@bob.com', datetime.now(UTC)) list_admins_response = ListAdminsResponse([user1, user2]) mocked_responses.add( responses.GET, 'http://test.com/groups/foo/admins', @@ -147,8 +147,8 @@ def test_list_group_admins(mocked_responses, vinyldns_client): def test_list_group_changes(mocked_responses, vinyldns_client): - change1 = GroupChange(sample_group, 'Create', 'user', None, 'id', datetime.datetime.utcnow()) - change2 = GroupChange(sample_group2, 'Update', 'user', sample_group, 'id2', datetime.datetime.utcnow()) + change1 = GroupChange(sample_group, 'Create', 'user', None, 'id', datetime.now(UTC)) + change2 = GroupChange(sample_group2, 'Update', 'user', sample_group, 'id2', datetime.now(UTC)) list_group_changes_response = ListGroupChangesResponse([change1, change2], 'start', 'next', 100) mocked_responses.add( responses.GET, 'http://test.com/groups/foo/activity?startFrom=start&maxItems=100', diff --git a/tests/test_zones.py b/tests/test_zones.py index 0519efa..44f6f98 100644 --- a/tests/test_zones.py +++ b/tests/test_zones.py @@ -13,7 +13,7 @@ # limitations under the License. import json -from datetime import datetime +from datetime import datetime, UTC import responses @@ -135,9 +135,9 @@ def test_get_zone_by_name(mocked_responses, vinyldns_client): def test_list_zone_changes(mocked_responses, vinyldns_client): change1 = ZoneChange(zone=forward_zone, user_id='some-user', change_type='Create', status='Pending', - created=datetime.utcnow(), system_message=None, id='zone-change-id1') + created=datetime.now(UTC), system_message=None, id='zone-change-id1') change2 = ZoneChange(zone=ip4_zone, user_id='some-user', change_type='Create', status='Pending', - created=datetime.utcnow(), system_message='msg', id='zone-change-id2') + created=datetime.now(UTC), system_message='msg', id='zone-change-id2') lzcr = ListZoneChangesResponse(forward_zone.id, [change1, change2], 'next', 'start', 100) mocked_responses.add( responses.GET, f'http://test.com/zones/{forward_zone.id}/changes?startFrom=start&maxItems=100', diff --git a/tox.ini b/tox.ini index 21ecd04..dc2a416 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ -; tox configuration for VinylDNS Python project +; tox configuration for VinylDNS Python project, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist + [tox] envlist = clean, @@ -23,7 +24,7 @@ deps = boto3>=1.26.0 commands = - pytest --cov=src --cov-report=term-missing -vv tests + pytest --cov=vinyldns --cov-report=term-missing -vv tests [testenv:check] deps = @@ -40,10 +41,9 @@ commands = coverage html [testenv:clean] +commands = coverage erase deps = coverage>=7.3 skip_install = true -commands = - coverage erase [testenv:func_test] commands_pre = From 1b3c818a7ddce1a4f9e992cc19d06c9745a11bcd Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Sun, 21 Dec 2025 21:00:52 +0530 Subject: [PATCH 4/7] update Signed-off-by: Jay07GIT --- src/vinyldns/client.py | 11 +++-------- src/vinyldns/record.py | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/vinyldns/client.py b/src/vinyldns/client.py index fa49ad3..266fdcc 100644 --- a/src/vinyldns/client.py +++ b/src/vinyldns/client.py @@ -20,16 +20,11 @@ import requests from datetime import datetime, UTC -from future.moves.urllib.parse import parse_qs -from future.utils import iteritems +from urllib.parse import parse_qs, urljoin, urlparse, urlsplit + from requests.adapters import HTTPAdapter -# Python 2/3 compatibility -from requests.compat import urljoin -from requests.compat import urlparse -from requests.compat import urlsplit -from requests.packages.urllib3.util.retry import Retry +from urllib3.util.retry import Retry -# TODO: Didn't like this boto request signer, fix when moving back from vinyldns.boto_request_signer import BotoRequestSigner from vinyldns.batch_change import BatchChange, ListBatchChangeSummaries, to_review_json diff --git a/src/vinyldns/record.py b/src/vinyldns/record.py index 141dc25..91f91b5 100644 --- a/src/vinyldns/record.py +++ b/src/vinyldns/record.py @@ -180,7 +180,7 @@ def from_dict(d): class RecordSet(object): def __init__(self, zone_id, name, type, ttl, status=None, created=None, - updated=None, records=[], id=None, owner_group_id=None,fqdn=None): + updated=None, records=[], id=None, owner_group_id=None, fqdn=None): self.zone_id = zone_id self.name = name self.type = type @@ -191,7 +191,7 @@ def __init__(self, zone_id, name, type, ttl, status=None, created=None, self.records = records self.id = id self.owner_group_id = owner_group_id - self.fqdn=fqdn + self.fqdn = fqdn @staticmethod def from_dict(d): From fff27520843fc54b544d1b4adc58409f7c1d1347 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Tue, 23 Dec 2025 19:46:04 +0530 Subject: [PATCH 5/7] update Signed-off-by: Jay07GIT --- src/vinyldns/boto_request_signer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vinyldns/boto_request_signer.py b/src/vinyldns/boto_request_signer.py index dded002..94cc52f 100644 --- a/src/vinyldns/boto_request_signer.py +++ b/src/vinyldns/boto_request_signer.py @@ -47,8 +47,8 @@ def __init__( if host is None: raise ValueError(f"Invalid index_url (missing host): {index_url}") - self.netloc = f"{host}: {port}" if port else host - self.base_url = f"{scheme}: //{self.netloc}" + self.netloc = f"{host}:{port}" if port else host + self.base_url = f"{scheme}://{self.netloc}" self.region_name = "us-east-1" self.service_name = "VinylDNS" self.credentials = Credentials(access_key, secret_access_key) From 0178b9f5937c5f3bae5245129c780f77fa5ad64c Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Wed, 24 Dec 2025 13:48:29 +0530 Subject: [PATCH 6/7] update Signed-off-by: Jay07GIT --- src/vinyldns/boto_request_signer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vinyldns/boto_request_signer.py b/src/vinyldns/boto_request_signer.py index 94cc52f..134c647 100644 --- a/src/vinyldns/boto_request_signer.py +++ b/src/vinyldns/boto_request_signer.py @@ -97,6 +97,8 @@ def build_auth_header( """ hdrs: Dict[str, str] = dict(headers or {}) hdrs.setdefault("Host", self.netloc) + # Remove Date header if present + hdrs.pop("Date", None) # Normalize body to bytes if body is None: From 9b823be443cfcc94ca700140ace93773b0f5d8d5 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 2 Jan 2026 22:53:06 +0530 Subject: [PATCH 7/7] update Signed-off-by: Jay07GIT --- .github/workflows/verify.yml | 1 + setup.py | 8 ++++---- src/vinyldns/boto_request_signer.py | 2 -- src/vinyldns/client.py | 2 -- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index a166d4c..10913cc 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -31,4 +31,5 @@ jobs: if: success() uses: codecov/codecov-action@v4 with: + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/setup.py b/setup.py index 8d6aea4..2030a5c 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ setup( name="vinyldns-python", - version="0.9.7", + version="0.9.8", packages=find_packages("src"), package_dir={"": "src"}, py_modules=[splitext(basename(path))[0] for path in glob("src/*.py")], @@ -51,7 +51,7 @@ "python-dateutil>=2.7.5", ], tests_require=[ - "responses==0.10.4", - "pytest==3.10.1", - ], + "responses==0.25.8", + "pytest==9.0.2", + ] ) diff --git a/src/vinyldns/boto_request_signer.py b/src/vinyldns/boto_request_signer.py index 134c647..879fd7b 100644 --- a/src/vinyldns/boto_request_signer.py +++ b/src/vinyldns/boto_request_signer.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations - import logging from datetime import datetime, UTC from typing import Dict, Optional, Union diff --git a/src/vinyldns/client.py b/src/vinyldns/client.py index 266fdcc..8f4c86e 100644 --- a/src/vinyldns/client.py +++ b/src/vinyldns/client.py @@ -16,8 +16,6 @@ import json import logging import os -from builtins import str - import requests from datetime import datetime, UTC from urllib.parse import parse_qs, urljoin, urlparse, urlsplit