From cdaf1a3826a408dca04c22a098e1a853b3aa082c Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Mon, 30 Apr 2018 21:27:38 -0700 Subject: [PATCH 01/34] Add stub for core.jsonify_search_results --- src/pynuget/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index 009cc20..915912d 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -212,3 +212,7 @@ def parse_nuspec(nuspec, ns=None): raise ApiException("api_error: ID or version missing") # TODO return metadata, pkg_name.text, version.text + + +def jsonify_search_results(): + pass From ef0614445e4271ee392fc8336170c85e9d7f9b4a Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Mon, 30 Apr 2018 21:29:32 -0700 Subject: [PATCH 02/34] Add start of test for core.jsonify_search_results Add JsonSchema for NuGet API v3: SearchQueryService --- requirements-dev.txt | 1 + ...uGet.SearchQueryService.v3.JsonSchema.json | 58 +++++++++++++++++++ tests/test_core.py | 45 ++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 tests/data/NuGet.SearchQueryService.v3.JsonSchema.json diff --git a/requirements-dev.txt b/requirements-dev.txt index 5ddcfe6..5ead8a8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1,2 @@ pytest == 3.5.0 +jsonschema == 2.6.0 diff --git a/tests/data/NuGet.SearchQueryService.v3.JsonSchema.json b/tests/data/NuGet.SearchQueryService.v3.JsonSchema.json new file mode 100644 index 0000000..98fa306 --- /dev/null +++ b/tests/data/NuGet.SearchQueryService.v3.JsonSchema.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/schema#", + "id": "NuGet.SearchQueryService.v3.json", + + "definitions": { + "str_or_array_of_str": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + + "version_data": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "downloads": { "type": "integer" } + }, + "required": ["@id", "version", "downloads"] + }, + + "pkg_data": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "description": { "type": "string" }, + "versions": { + "type": "array", + "items": { "$ref": "#/definitions/version_data" } + }, + "authors": { "$ref": "#/definitions/str_or_array_of_str" }, + "iconUrl": { "type": "string" }, + "licenseUrl": { "type": "string" }, + "owners": { "$ref": "#/definitions/str_or_array_of_str" }, + "projectUrl": { "type": "string" }, + "registration": { "type": "string" }, + "summary": { "type": "string" }, + "tags": { "$ref": "#/definitions/str_or_array_of_str" }, + "title": { "type": "string" }, + "totalDownloads": { "type": "integer" }, + "verified": { "type": "boolean" } + }, + "required": [ "id", "version", "versions" ] + } + }, + + "type": "object", + "properties": { + "totalHits": { "type": "integer" }, + "data": { + "type": "array", + "items": { "$ref": "#/definitions/pkg_data" } + } + }, + "required": [ "totalHits", "data" ] +} \ No newline at end of file diff --git a/tests/test_core.py b/tests/test_core.py index 0279542..c759a21 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,16 +1,20 @@ # -*- coding: utf-8 -*- """ """ +import json import shutil from unittest.mock import MagicMock import xml.etree.ElementTree as et import os import pytest +from jsonschema import validate +from jsonschema.exceptions import ValidationError from werkzeug.datastructures import FileStorage from pynuget import core from pynuget import app +from pynuget import db DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") NAMESPACE = {'nuspec': @@ -131,3 +135,44 @@ def test_determine_dependencies(): {'framework': None, 'id': 'E', 'version': '0.0.5'}, ] assert result == expected + + +@pytest.mark.temp +def test_jsonify_search_results(): + search_results = [ + db.Version(), + db.Version(), + db.Version(), + ] + + schema_file = os.path.join( + DATA_DIR, + "NuGet.SearchQueryService.v3.JsonSchema.json", + ) + with open(schema_file, 'r') as openf: + schema = json.load(openf) +# results = core.jsonify_search_results(search_results) + results = { + "totalHits": 1, + "data": [ + { + "@id": "some_url", + "@type": "Package", + "registration": "some_url", + "id": "NuGet.Versioning", + "version": "4.4.0", + "versions": [ + { + "version": "4.4.0", + "downloads": 617, + "@id": "some_url" + } + ] + } + ] + } + + try: + validate(results, schema) + except ValidationError: + pytest.fail("Invalid JSON") From 1e5e2fa6d2cdea6adbae04a363d4fd501206fc6f Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Mon, 30 Apr 2018 21:31:00 -0700 Subject: [PATCH 03/34] Add stub for route.search_v3 --- src/pynuget/routes.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/pynuget/routes.py b/src/pynuget/routes.py index de762f5..8bbc7e1 100644 --- a/src/pynuget/routes.py +++ b/src/pynuget/routes.py @@ -275,6 +275,28 @@ def search(): return resp +@app.route('/search_v3', methods=['GET']) +def search_v3(): + logger.debug("Route: /search_v3") + q = request.args.get('q', default=None) + skip = request.args.get('skip', default=0) + take = request.args.get('take', default=None) + include_prerelease = request.args.get('prerelease', default=False) + sem_ver_level = request.args.get('semVerLevel', default=None) + + results = db.search_packages(session, + include_prerelease, + filter_=None, + search_query=None, + ) + + # jsonify the results. + data = "" + + return data, 200 + + + @app.route('/updates', methods=['GET']) def updates(): logger.debug("Route: /updates") From 4a1f683a29c651e53b3576e9efd17b32971dce27 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 07:33:07 -0700 Subject: [PATCH 04/34] Add static/index.json --- static/index.json | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 static/index.json diff --git a/static/index.json b/static/index.json new file mode 100644 index 0000000..40436ef --- /dev/null +++ b/static/index.json @@ -0,0 +1,51 @@ +{ + "version": "3.0.0", + "resources": [ + { + "@id": "http://localhost:5000/query", + "@type": "SearchQueryService", + "comment": "Query endpoint of NuGet Search service (primary)" + }, + { + "@id": "http://localhost:5000/v3/registration3/", + "@type": "RegistrationsBaseUrl", + "comment": "Base URL of Azure storage where NuGet package registration info is stored" + }, + { + "@id": "http://localhost:5000/v3-flatcontainer/", + "@type": "PackageBaseAddress/3.0.0", + "comment": "Base URL of where NuGet packages are stored, in the format https://api.nuget.org/v3-flatcontainer/{id-lower}/{version-lower}/{id-lower}.{version-lower}.nupkg" + }, + { + "@id": "http://localhost:5000/api/v2", + "@type": "LegacyGallery" + }, + { + "@id": "http://localhost:5000/api/v2", + "@type": "LegacyGallery/2.0.0" + }, + { + "@id": "http://localhost:5000/api/v2/package", + "@type": "PackagePublish/2.0.0" + }, + { + "@id": "https://api.nuget.org/v3/registration3-gz/", + "@type": "RegistrationsBaseUrl/3.4.0", + "comment": "Base URL of Azure storage where NuGet package registration info is stored in GZIP format. This base URL does not include SemVer 2.0.0 packages." + }, + { + "@id": "https://api.nuget.org/v3/registration3-gz-semver2/", + "@type": "RegistrationsBaseUrl/3.6.0", + "comment": "Base URL of Azure storage where NuGet package registration info is stored in GZIP format. This base URL includes SemVer 2.0.0 packages." + }, + { + "@id": "https://api.nuget.org/v3/catalog0/index.json", + "@type": "Catalog/3.0.0", + "comment": "Index of the NuGet package catalog." + } + ], + "@context": { + "@vocab": "http://schema.nuget.org/services#", + "comment": "http://www.w3.org/2000/01/rdf-schema#comment" + } +} \ No newline at end of file From de84cfedb2ff5c975f43a1a4d7388a7f0f15d303 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 08:26:47 -0700 Subject: [PATCH 05/34] Renamed NuGet.Api.v3.SearchQueryService.json --- ....v3.JsonSchema.json => NuGet.Api.v3.SearchQueryService.json} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/data/{NuGet.SearchQueryService.v3.JsonSchema.json => NuGet.Api.v3.SearchQueryService.json} (97%) diff --git a/tests/data/NuGet.SearchQueryService.v3.JsonSchema.json b/tests/data/NuGet.Api.v3.SearchQueryService.json similarity index 97% rename from tests/data/NuGet.SearchQueryService.v3.JsonSchema.json rename to tests/data/NuGet.Api.v3.SearchQueryService.json index 98fa306..178c431 100644 --- a/tests/data/NuGet.SearchQueryService.v3.JsonSchema.json +++ b/tests/data/NuGet.Api.v3.SearchQueryService.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema#", - "id": "NuGet.SearchQueryService.v3.json", + "id": "NuGet.Api.v3.SearchQueryService.json", "definitions": { "str_or_array_of_str": { From 7b71f4ea6233cf24e378530bbdf96202e323b99a Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 14:50:52 -0700 Subject: [PATCH 06/34] Add stubs for various NuGetResponse objects --- src/pynuget/core.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index 915912d..140b37f 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -24,6 +24,33 @@ class ApiException(PyNuGetException): pass +class NuGetResponse(object): + + def to_json(self): + """Return the object as JSON to send to NuGet""" + pass + + +class ServiceIndex(NuGetResponse): + pass + + +class SearchResponse(NuGetResponse): + pass + + +class MetadataResponse(NuGetResponse): + pass + + +class ContentResponse(NuGetResponse): + pass + + +class CatalogResponse(NuGetResponse): + pass + + # XXX: Not needed? def api_error(): raise NotImplementedError From a6ac7a3ce3314f2ca775a2d9f92eb54cc14aad02 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 14:55:49 -0700 Subject: [PATCH 07/34] Add core.ServiceIndexResource --- src/pynuget/core.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index 140b37f..d71da48 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -35,6 +35,30 @@ class ServiceIndex(NuGetResponse): pass +class ServiceIndexResource(NuGetResponse): + + def __init__(self, url, resource_type, comment=None): + """ + url : str + resource_type : str + comment : str, optional + """ + self.url = url + self.resource_type = resource_type + self.comment = comment + + def json_mapping(self): + """Defines how the python objects map to JSON""" + mapping = { + "@id": self.url, + "@type": self.resource_type, + } + if self.comment is not None: + mapping['comment'] = self.comment + + return mapping + + class SearchResponse(NuGetResponse): pass From b04a492ddd9689023fce8aff7d4bf78d49531ecb Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 14:58:29 -0700 Subject: [PATCH 08/34] Fill in core.ServiceIndex --- src/pynuget/core.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index d71da48..4813207 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -32,7 +32,21 @@ def to_json(self): class ServiceIndex(NuGetResponse): - pass + + def __init__(self, version, resources): + """ + version : str + resources : list of :class:`ServiceIndexResource` + """ + self.version = version + self.resources = resources + + def json_mapping(self): + mapping = { + "version": self.version, + "resources": self.resources, + } + return mapping class ServiceIndexResource(NuGetResponse): From 5ed507605f43607d42e4d5a47ed13b75e43b397a Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 14:59:14 -0700 Subject: [PATCH 09/34] Responses should have 'Response' in the class name --- src/pynuget/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index 4813207..5f1b5aa 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -31,7 +31,7 @@ def to_json(self): pass -class ServiceIndex(NuGetResponse): +class ServiceIndexResponse(NuGetResponse): def __init__(self, version, resources): """ @@ -49,7 +49,7 @@ def json_mapping(self): return mapping -class ServiceIndexResource(NuGetResponse): +class ServiceIndexResourceResponse(NuGetResponse): def __init__(self, url, resource_type, comment=None): """ From fc241d2bef1e7441f41247c7e6d9ab1bc19e83cc Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 15:10:58 -0700 Subject: [PATCH 10/34] fill in basic NuGetResponse.to_json() method --- src/pynuget/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index 5f1b5aa..140c830 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -3,6 +3,7 @@ """ import base64 import hashlib +import json import os import re import shutil @@ -28,7 +29,7 @@ class NuGetResponse(object): def to_json(self): """Return the object as JSON to send to NuGet""" - pass + return json.dumps(self.json_mapping(), sort_keys=True) class ServiceIndexResponse(NuGetResponse): From 05d016a4cbfe20d226524509a75558bf9289164f Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 15:11:31 -0700 Subject: [PATCH 11/34] Skip the JsonSchema test for now, perhaps for good --- tests/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_core.py b/tests/test_core.py index c759a21..a030b5f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -137,7 +137,7 @@ def test_determine_dependencies(): assert result == expected -@pytest.mark.temp +@pytest.mark.skip("I moved the file") def test_jsonify_search_results(): search_results = [ db.Version(), From 80fe8bf1fa043edb3ef36869c170549ee8f5b5ce Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 15:13:39 -0700 Subject: [PATCH 12/34] Add test_to_json_ServiceIndexResourceReponse --- tests/test_core.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index a030b5f..dadaaf0 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -176,3 +176,31 @@ def test_jsonify_search_results(): validate(results, schema) except ValidationError: pytest.fail("Invalid JSON") + + +class TestNuGetResponse(): + + def test_to_json_ServiceIndexResourceResponse(self): + obj = core.ServiceIndexResourceResponse( + url="https://api.nuget.org/v3-flatcontainer/", + resource_type="PackageBaseAddress/3.0.0", + ) + + expected = ('{"@id": "https://api.nuget.org/v3-flatcontainer/",' + ' "@type": "PackageBaseAddress/3.0.0"}') + assert obj.to_json() == expected + + obj = core.ServiceIndexResourceResponse( + url="https://api.nuget.org/v3-flatcontainer/", + resource_type="PackageBaseAddress/3.0.0", + comment="foo", + ) + + expected = ( + '{' + '"@id": "https://api.nuget.org/v3-flatcontainer/",' + ' "@type": "PackageBaseAddress/3.0.0",' + ' "comment": "foo"' + '}' + ) + assert obj.to_json() == expected From 2b33766b8c39de5ee03d7149c64847aecfcb6deb Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 15:31:47 -0700 Subject: [PATCH 13/34] Use a .json property instead of to_json() method --- src/pynuget/core.py | 3 ++- tests/test_core.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index 140c830..ff92a98 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -27,7 +27,8 @@ class ApiException(PyNuGetException): class NuGetResponse(object): - def to_json(self): + @property + def json(self): """Return the object as JSON to send to NuGet""" return json.dumps(self.json_mapping(), sort_keys=True) diff --git a/tests/test_core.py b/tests/test_core.py index dadaaf0..e901368 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -188,7 +188,7 @@ def test_to_json_ServiceIndexResourceResponse(self): expected = ('{"@id": "https://api.nuget.org/v3-flatcontainer/",' ' "@type": "PackageBaseAddress/3.0.0"}') - assert obj.to_json() == expected + assert obj.json == expected obj = core.ServiceIndexResourceResponse( url="https://api.nuget.org/v3-flatcontainer/", @@ -203,4 +203,4 @@ def test_to_json_ServiceIndexResourceResponse(self): ' "comment": "foo"' '}' ) - assert obj.to_json() == expected + assert obj.json == expected From e1a4a3aec46ef8772f9bd80cc3b78f7e070b86e5 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 16:36:20 -0700 Subject: [PATCH 14/34] Add test_to_json_ServiceIndexResponse --- tests/test_core.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index e901368..132f097 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -204,3 +204,25 @@ def test_to_json_ServiceIndexResourceResponse(self): '}' ) assert obj.json == expected + + def test_to_json_ServiceIndexResponse(self): + resources = [ + core.ServiceIndexResourceResponse("a", "b"), + core.ServiceIndexResourceResponse("c", "d", "e"), + ] + obj = core.ServiceIndexResponse( + version="3.0.0", + resources=resources, + ) + + expected = ( + '{' + '"resources": [' + '{"@id": "a", "@type": "b"},' + '{"@id": "a", "@type": "b", "comment": "c"}' + '], "version": "3.0.0"' + '}' + ) + assert obj.json == expected + print(obj.json_mapping()) + pytest.fail() From b404091cc18803c71d5dd171dc14c157bba78de0 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 16:36:50 -0700 Subject: [PATCH 15/34] REBASE ME. working on the json mapping --- src/pynuget/core.py | 56 ++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index ff92a98..edbd721 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -30,7 +30,22 @@ class NuGetResponse(object): @property def json(self): """Return the object as JSON to send to NuGet""" - return json.dumps(self.json_mapping(), sort_keys=True) + encoder = json.JSONEncoder(sort_keys=True, default=self._map_json) + return encoder.encode(self.__dict__) + + def _map_json(self, obj): + # TODO: recursion + for key in obj.keys(): + # Remove keys that have None values. + if obj[key] is None: + obj.pop(key) + continue + # Rename ones that need to be renamed + if hasattr(self, 'json_map') and key in self.json_map.keys(): + new_key = self.json_map[key] + obj[new_key] = obj[key] + obj.pop(key) + return obj class ServiceIndexResponse(NuGetResponse): @@ -38,21 +53,26 @@ class ServiceIndexResponse(NuGetResponse): def __init__(self, version, resources): """ version : str - resources : list of :class:`ServiceIndexResource` + resources : list of :class:`ServiceIndexResourceResponse` """ self.version = version self.resources = resources - def json_mapping(self): - mapping = { - "version": self.version, - "resources": self.resources, - } - return mapping +# def json_mapping(self): +# mapping = { +# "version": self.version, +# "resources": [r.json for r in self.resources], +# } +# return mapping class ServiceIndexResourceResponse(NuGetResponse): + json_map = { + "url": "@id", + "resouce_type": "@type" + } + def __init__(self, url, resource_type, comment=None): """ url : str @@ -63,16 +83,16 @@ def __init__(self, url, resource_type, comment=None): self.resource_type = resource_type self.comment = comment - def json_mapping(self): - """Defines how the python objects map to JSON""" - mapping = { - "@id": self.url, - "@type": self.resource_type, - } - if self.comment is not None: - mapping['comment'] = self.comment - - return mapping +# def json_mapping(self): +# """Defines how the python objects map to JSON""" +# mapping = { +# "@id": self.url, +# "@type": self.resource_type, +# } +# if self.comment is not None: +# mapping['comment'] = self.comment +# +# return mapping class SearchResponse(NuGetResponse): From 3ecf7d8b9186665414f8169bdcbb5f76ad49cd8a Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 19:05:16 -0700 Subject: [PATCH 16/34] I think I have a working json encoder... --- src/pynuget/core.py | 77 ++++++++++++++++++++++++--------------------- tests/test_core.py | 5 ++- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index edbd721..68d85d6 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -30,22 +30,45 @@ class NuGetResponse(object): @property def json(self): """Return the object as JSON to send to NuGet""" - encoder = json.JSONEncoder(sort_keys=True, default=self._map_json) - return encoder.encode(self.__dict__) - - def _map_json(self, obj): - # TODO: recursion - for key in obj.keys(): - # Remove keys that have None values. - if obj[key] is None: - obj.pop(key) - continue - # Rename ones that need to be renamed - if hasattr(self, 'json_map') and key in self.json_map.keys(): - new_key = self.json_map[key] - obj[new_key] = obj[key] - obj.pop(key) - return obj + logger.debug("in json property") + encoder = json.JSONEncoder(sort_keys=True, default=self._rename_keys) + logger.debug("set encoder. Obj: {}".format(self)) + return encoder.encode(self) + + def _rename_keys(self, obj): + logger.debug("renaming keys for {}".format(obj)) + + if hasattr(obj, '__dict__'): + logger.debug("has __dict__ = {}".format(obj.__dict__)) + if hasattr(obj, 'json_key_map'): + logger.debug("has json_key_map") + d = obj.__dict__ + + # wrap in list() because we're going to be modifying things + items = list(d.items()) + logger.debug(items) + for key, value in items: + # Delete items with no value + if value is None: + logger.debug("deleting key '{}' with value `{}`".format(key, value)) + del d[key] + continue + + # Map our python names to the NuGet API names. + if key in obj.json_key_map.keys(): + new_key = obj.json_key_map[key] + logger.debug("renaming key `{}` to `{}`".format(key, new_key)) + d[new_key] = d[key] + del d[key] + return d + else: + logger.debug("does not have json_key_map") + # Return the dict of the item we're processing. It will then + # continue on through the Encoder. When another unencodable + # object is reached, _rename_keys() will be called again. + return obj.__dict__ + else: + raise PyNuGetException("Doug, fix this") class ServiceIndexResponse(NuGetResponse): @@ -58,19 +81,12 @@ def __init__(self, version, resources): self.version = version self.resources = resources -# def json_mapping(self): -# mapping = { -# "version": self.version, -# "resources": [r.json for r in self.resources], -# } -# return mapping - class ServiceIndexResourceResponse(NuGetResponse): - json_map = { + json_key_map = { "url": "@id", - "resouce_type": "@type" + "resource_type": "@type" } def __init__(self, url, resource_type, comment=None): @@ -83,17 +99,6 @@ def __init__(self, url, resource_type, comment=None): self.resource_type = resource_type self.comment = comment -# def json_mapping(self): -# """Defines how the python objects map to JSON""" -# mapping = { -# "@id": self.url, -# "@type": self.resource_type, -# } -# if self.comment is not None: -# mapping['comment'] = self.comment -# -# return mapping - class SearchResponse(NuGetResponse): pass diff --git a/tests/test_core.py b/tests/test_core.py index 132f097..fb36ffc 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -213,16 +213,15 @@ def test_to_json_ServiceIndexResponse(self): obj = core.ServiceIndexResponse( version="3.0.0", resources=resources, +# resources=None, ) expected = ( '{' '"resources": [' '{"@id": "a", "@type": "b"},' - '{"@id": "a", "@type": "b", "comment": "c"}' + ' {"@id": "c", "@type": "d", "comment": "e"}' '], "version": "3.0.0"' '}' ) assert obj.json == expected - print(obj.json_mapping()) - pytest.fail() From e91f1a153a72be05ae35d9f6ca4d989f90189769 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 19:05:40 -0700 Subject: [PATCH 17/34] Stubs for SearchResultResponse and SearchResultVersionResponse --- src/pynuget/core.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index 68d85d6..5c932ee 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -104,6 +104,14 @@ class SearchResponse(NuGetResponse): pass +class SearchResultResponse(NuGetResponse): + pass + + +class SearchResultVersionResponse(NuGetResponse): + pass + + class MetadataResponse(NuGetResponse): pass From e8f764b373881eee6554a4770154c13349581456 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 19:23:11 -0700 Subject: [PATCH 18/34] Fill in the SearchResult classes --- src/pynuget/core.py | 50 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index 5c932ee..e75b44f 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -101,15 +101,59 @@ def __init__(self, url, resource_type, comment=None): class SearchResponse(NuGetResponse): - pass + + json_key_map = { + "total_hits": "totalHits", + } + + def __init__(self, total_hits, data): + """ + total_hits : int + data : list of :class:`SearchResultResponse` + """ + self.total_hits = total_hits + self.data = data class SearchResultResponse(NuGetResponse): - pass + + json_key_map = { + "id_": "id", + "icon_url": "iconUrl", + "license_url": "licenseUrl", + "project_url": "projectUrl", + "total_downloads:": "totalDownloads", + } + + def __init__(self, id_, version, versions, **kwargs): + """ + id_ : str + version : str + versions : list of :class:`SearchResultVersionResponse` + """ + # description, authors, + # icon_url, license_url, owners, project_url, registration, + # summary, tags, title, total_downloads, verified + self.id_ = id_ + self.version = version + self.versions = versions class SearchResultVersionResponse(NuGetResponse): - pass + + json_key_map = { + "id_": "@id", + } + + def __init__(self, id_, version, downloads): + """ + id_ : str + version : str + downloads : int + """ + self.id_ = id_ + self.version = version + self.downloads = downloads class MetadataResponse(NuGetResponse): From 2566d1dff290738dcfd20f8d0bde57f41002084b Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 19:23:30 -0700 Subject: [PATCH 19/34] Remove dummy line --- tests/test_core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_core.py b/tests/test_core.py index fb36ffc..9a039d8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -213,7 +213,6 @@ def test_to_json_ServiceIndexResponse(self): obj = core.ServiceIndexResponse( version="3.0.0", resources=resources, -# resources=None, ) expected = ( From 0aa89e8526d4794adf095c3eb80e384f40ff8df8 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 19:26:34 -0700 Subject: [PATCH 20/34] Finished tests for SearchResponse --- tests/test_core.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index 9a039d8..b2adf5b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -224,3 +224,35 @@ def test_to_json_ServiceIndexResponse(self): '}' ) assert obj.json == expected + + def test_json_SearchResponse(self): + + versions_a = [ + core.SearchResultVersionResponse("a", "b", 100), + core.SearchResultVersionResponse("c", "d", 50), + ] + versions_b = [ + core.SearchResultVersionResponse("e", "f", 150), + core.SearchResultVersionResponse("g", "h", 250), + ] + + data = [ + core.SearchResultResponse(1, 2, versions_a), + core.SearchResultResponse(4, 5, versions_b), + ] + obj = core.SearchResponse( + total_hits=561, + data=data, + ) + + expected = ( + '{"data":' + ' [{"id": 1, "version": 2, "versions":' + ' [{"@id": "a", "downloads": 100, "version": "b"},' + ' {"@id": "c", "downloads": 50, "version": "d"}]},' + ' {"id": 4, "version": 5, "versions":' + ' [{"@id": "e", "downloads": 150, "version": "f"},' + ' {"@id": "g", "downloads": 250, "version": "h"}]}],' + ' "totalHits": 561}' + ) + assert obj.json == expected From 7ae37e57857b3757c30b817d8d725be4f4b9eb0a Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 19:28:09 -0700 Subject: [PATCH 21/34] Use one-line expected values where we can --- tests/test_core.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index b2adf5b..024c901 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -178,31 +178,25 @@ def test_jsonify_search_results(): pytest.fail("Invalid JSON") +@pytest.mark.temp class TestNuGetResponse(): def test_to_json_ServiceIndexResourceResponse(self): obj = core.ServiceIndexResourceResponse( - url="https://api.nuget.org/v3-flatcontainer/", - resource_type="PackageBaseAddress/3.0.0", + url="aaa", + resource_type="bbb", ) - expected = ('{"@id": "https://api.nuget.org/v3-flatcontainer/",' - ' "@type": "PackageBaseAddress/3.0.0"}') + expected = '{"@id": "aaa", "@type": "bbb"}' assert obj.json == expected obj = core.ServiceIndexResourceResponse( - url="https://api.nuget.org/v3-flatcontainer/", - resource_type="PackageBaseAddress/3.0.0", + url="ccc", + resource_type="ddd", comment="foo", ) - expected = ( - '{' - '"@id": "https://api.nuget.org/v3-flatcontainer/",' - ' "@type": "PackageBaseAddress/3.0.0",' - ' "comment": "foo"' - '}' - ) + expected = '{"@id": "ccc", "@type": "ddd", "comment": "foo"}' assert obj.json == expected def test_to_json_ServiceIndexResponse(self): From 124c621bd47e95ed2c406bc324606d98cbd8c69f Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 1 May 2018 19:30:12 -0700 Subject: [PATCH 22/34] Remove all the temporary debug logging --- src/pynuget/core.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/pynuget/core.py b/src/pynuget/core.py index e75b44f..cfcbb53 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -30,39 +30,29 @@ class NuGetResponse(object): @property def json(self): """Return the object as JSON to send to NuGet""" - logger.debug("in json property") encoder = json.JSONEncoder(sort_keys=True, default=self._rename_keys) - logger.debug("set encoder. Obj: {}".format(self)) return encoder.encode(self) def _rename_keys(self, obj): - logger.debug("renaming keys for {}".format(obj)) - if hasattr(obj, '__dict__'): - logger.debug("has __dict__ = {}".format(obj.__dict__)) if hasattr(obj, 'json_key_map'): - logger.debug("has json_key_map") d = obj.__dict__ # wrap in list() because we're going to be modifying things items = list(d.items()) - logger.debug(items) for key, value in items: # Delete items with no value if value is None: - logger.debug("deleting key '{}' with value `{}`".format(key, value)) del d[key] continue # Map our python names to the NuGet API names. if key in obj.json_key_map.keys(): new_key = obj.json_key_map[key] - logger.debug("renaming key `{}` to `{}`".format(key, new_key)) d[new_key] = d[key] del d[key] return d else: - logger.debug("does not have json_key_map") # Return the dict of the item we're processing. It will then # continue on through the Encoder. When another unencodable # object is reached, _rename_keys() will be called again. From 1a7bb5cd0c3f9586d1a96b7c23c725386bbd286c Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Wed, 2 May 2018 09:55:20 -0700 Subject: [PATCH 23/34] Move the NuGetResponse classes to their own file. --- src/pynuget/core.py | 133 -------------------------------- src/pynuget/nuget_response.py | 140 ++++++++++++++++++++++++++++++++++ tests/test_core.py | 74 ------------------ tests/test_nuget_response.py | 81 ++++++++++++++++++++ 4 files changed, 221 insertions(+), 207 deletions(-) create mode 100644 src/pynuget/nuget_response.py create mode 100644 tests/test_nuget_response.py diff --git a/src/pynuget/core.py b/src/pynuget/core.py index cfcbb53..f13c89a 100644 --- a/src/pynuget/core.py +++ b/src/pynuget/core.py @@ -25,139 +25,6 @@ class ApiException(PyNuGetException): pass -class NuGetResponse(object): - - @property - def json(self): - """Return the object as JSON to send to NuGet""" - encoder = json.JSONEncoder(sort_keys=True, default=self._rename_keys) - return encoder.encode(self) - - def _rename_keys(self, obj): - if hasattr(obj, '__dict__'): - if hasattr(obj, 'json_key_map'): - d = obj.__dict__ - - # wrap in list() because we're going to be modifying things - items = list(d.items()) - for key, value in items: - # Delete items with no value - if value is None: - del d[key] - continue - - # Map our python names to the NuGet API names. - if key in obj.json_key_map.keys(): - new_key = obj.json_key_map[key] - d[new_key] = d[key] - del d[key] - return d - else: - # Return the dict of the item we're processing. It will then - # continue on through the Encoder. When another unencodable - # object is reached, _rename_keys() will be called again. - return obj.__dict__ - else: - raise PyNuGetException("Doug, fix this") - - -class ServiceIndexResponse(NuGetResponse): - - def __init__(self, version, resources): - """ - version : str - resources : list of :class:`ServiceIndexResourceResponse` - """ - self.version = version - self.resources = resources - - -class ServiceIndexResourceResponse(NuGetResponse): - - json_key_map = { - "url": "@id", - "resource_type": "@type" - } - - def __init__(self, url, resource_type, comment=None): - """ - url : str - resource_type : str - comment : str, optional - """ - self.url = url - self.resource_type = resource_type - self.comment = comment - - -class SearchResponse(NuGetResponse): - - json_key_map = { - "total_hits": "totalHits", - } - - def __init__(self, total_hits, data): - """ - total_hits : int - data : list of :class:`SearchResultResponse` - """ - self.total_hits = total_hits - self.data = data - - -class SearchResultResponse(NuGetResponse): - - json_key_map = { - "id_": "id", - "icon_url": "iconUrl", - "license_url": "licenseUrl", - "project_url": "projectUrl", - "total_downloads:": "totalDownloads", - } - - def __init__(self, id_, version, versions, **kwargs): - """ - id_ : str - version : str - versions : list of :class:`SearchResultVersionResponse` - """ - # description, authors, - # icon_url, license_url, owners, project_url, registration, - # summary, tags, title, total_downloads, verified - self.id_ = id_ - self.version = version - self.versions = versions - - -class SearchResultVersionResponse(NuGetResponse): - - json_key_map = { - "id_": "@id", - } - - def __init__(self, id_, version, downloads): - """ - id_ : str - version : str - downloads : int - """ - self.id_ = id_ - self.version = version - self.downloads = downloads - - -class MetadataResponse(NuGetResponse): - pass - - -class ContentResponse(NuGetResponse): - pass - - -class CatalogResponse(NuGetResponse): - pass - - # XXX: Not needed? def api_error(): raise NotImplementedError diff --git a/src/pynuget/nuget_response.py b/src/pynuget/nuget_response.py new file mode 100644 index 0000000..dc14eb2 --- /dev/null +++ b/src/pynuget/nuget_response.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +""" +""" + +import json + +from pynuget.core import PyNuGetException + + +class NuGetResponse(object): + + @property + def json(self): + """Return the object as JSON to send to NuGet""" + encoder = json.JSONEncoder(sort_keys=True, default=self._rename_keys) + return encoder.encode(self) + + def _rename_keys(self, obj): + if hasattr(obj, '__dict__'): + if hasattr(obj, 'json_key_map'): + d = obj.__dict__ + + # wrap in list() because we're going to be modifying things + items = list(d.items()) + for key, value in items: + # Delete items with no value + if value is None: + del d[key] + continue + + # Map our python names to the NuGet API names. + if key in obj.json_key_map.keys(): + new_key = obj.json_key_map[key] + d[new_key] = d[key] + del d[key] + return d + else: + # Return the dict of the item we're processing. It will then + # continue on through the Encoder. When another unencodable + # object is reached, _rename_keys() will be called again. + return obj.__dict__ + else: + raise PyNuGetException("Doug, fix this") + + +class ServiceIndexResponse(NuGetResponse): + + def __init__(self, version, resources): + """ + version : str + resources : list of :class:`ServiceIndexResourceResponse` + """ + self.version = version + self.resources = resources + + +class ServiceIndexResourceResponse(NuGetResponse): + + json_key_map = { + "url": "@id", + "resource_type": "@type" + } + + def __init__(self, url, resource_type, comment=None): + """ + url : str + resource_type : str + comment : str, optional + """ + self.url = url + self.resource_type = resource_type + self.comment = comment + + +class SearchResponse(NuGetResponse): + + json_key_map = { + "total_hits": "totalHits", + } + + def __init__(self, total_hits, data): + """ + total_hits : int + data : list of :class:`SearchResultResponse` + """ + self.total_hits = total_hits + self.data = data + + +class SearchResultResponse(NuGetResponse): + + json_key_map = { + "id_": "id", + "icon_url": "iconUrl", + "license_url": "licenseUrl", + "project_url": "projectUrl", + "total_downloads:": "totalDownloads", + } + + def __init__(self, id_, version, versions, **kwargs): + """ + id_ : str + version : str + versions : list of :class:`SearchResultVersionResponse` + """ + # description, authors, + # icon_url, license_url, owners, project_url, registration, + # summary, tags, title, total_downloads, verified + self.id_ = id_ + self.version = version + self.versions = versions + + +class SearchResultVersionResponse(NuGetResponse): + + json_key_map = { + "id_": "@id", + } + + def __init__(self, id_, version, downloads): + """ + id_ : str + version : str + downloads : int + """ + self.id_ = id_ + self.version = version + self.downloads = downloads + + +class MetadataResponse(NuGetResponse): + pass + + +class ContentResponse(NuGetResponse): + pass + + +class CatalogResponse(NuGetResponse): + pass diff --git a/tests/test_core.py b/tests/test_core.py index 024c901..a030b5f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -176,77 +176,3 @@ def test_jsonify_search_results(): validate(results, schema) except ValidationError: pytest.fail("Invalid JSON") - - -@pytest.mark.temp -class TestNuGetResponse(): - - def test_to_json_ServiceIndexResourceResponse(self): - obj = core.ServiceIndexResourceResponse( - url="aaa", - resource_type="bbb", - ) - - expected = '{"@id": "aaa", "@type": "bbb"}' - assert obj.json == expected - - obj = core.ServiceIndexResourceResponse( - url="ccc", - resource_type="ddd", - comment="foo", - ) - - expected = '{"@id": "ccc", "@type": "ddd", "comment": "foo"}' - assert obj.json == expected - - def test_to_json_ServiceIndexResponse(self): - resources = [ - core.ServiceIndexResourceResponse("a", "b"), - core.ServiceIndexResourceResponse("c", "d", "e"), - ] - obj = core.ServiceIndexResponse( - version="3.0.0", - resources=resources, - ) - - expected = ( - '{' - '"resources": [' - '{"@id": "a", "@type": "b"},' - ' {"@id": "c", "@type": "d", "comment": "e"}' - '], "version": "3.0.0"' - '}' - ) - assert obj.json == expected - - def test_json_SearchResponse(self): - - versions_a = [ - core.SearchResultVersionResponse("a", "b", 100), - core.SearchResultVersionResponse("c", "d", 50), - ] - versions_b = [ - core.SearchResultVersionResponse("e", "f", 150), - core.SearchResultVersionResponse("g", "h", 250), - ] - - data = [ - core.SearchResultResponse(1, 2, versions_a), - core.SearchResultResponse(4, 5, versions_b), - ] - obj = core.SearchResponse( - total_hits=561, - data=data, - ) - - expected = ( - '{"data":' - ' [{"id": 1, "version": 2, "versions":' - ' [{"@id": "a", "downloads": 100, "version": "b"},' - ' {"@id": "c", "downloads": 50, "version": "d"}]},' - ' {"id": 4, "version": 5, "versions":' - ' [{"@id": "e", "downloads": 150, "version": "f"},' - ' {"@id": "g", "downloads": 250, "version": "h"}]}],' - ' "totalHits": 561}' - ) - assert obj.json == expected diff --git a/tests/test_nuget_response.py b/tests/test_nuget_response.py new file mode 100644 index 0000000..462b59c --- /dev/null +++ b/tests/test_nuget_response.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +""" +""" + +import pytest + +from pynuget import nuget_response as nr + + +@pytest.mark.temp +class TestNuGetResponse(): + + def test_to_json_ServiceIndexResourceResponse(self): + obj = nr.ServiceIndexResourceResponse( + url="aaa", + resource_type="bbb", + ) + + expected = '{"@id": "aaa", "@type": "bbb"}' + assert obj.json == expected + + obj = nr.ServiceIndexResourceResponse( + url="ccc", + resource_type="ddd", + comment="foo", + ) + + expected = '{"@id": "ccc", "@type": "ddd", "comment": "foo"}' + assert obj.json == expected + + def test_to_json_ServiceIndexResponse(self): + resources = [ + nr.ServiceIndexResourceResponse("a", "b"), + nr.ServiceIndexResourceResponse("c", "d", "e"), + ] + obj = nr.ServiceIndexResponse( + version="3.0.0", + resources=resources, + ) + + expected = ( + '{' + '"resources": [' + '{"@id": "a", "@type": "b"},' + ' {"@id": "c", "@type": "d", "comment": "e"}' + '], "version": "3.0.0"' + '}' + ) + assert obj.json == expected + + def test_json_SearchResponse(self): + + versions_a = [ + nr.SearchResultVersionResponse("a", "b", 100), + nr.SearchResultVersionResponse("c", "d", 50), + ] + versions_b = [ + nr.SearchResultVersionResponse("e", "f", 150), + nr.SearchResultVersionResponse("g", "h", 250), + ] + + data = [ + nr.SearchResultResponse(1, 2, versions_a), + nr.SearchResultResponse(4, 5, versions_b), + ] + obj = nr.SearchResponse( + total_hits=561, + data=data, + ) + + expected = ( + '{"data":' + ' [{"id": 1, "version": 2, "versions":' + ' [{"@id": "a", "downloads": 100, "version": "b"},' + ' {"@id": "c", "downloads": 50, "version": "d"}]},' + ' {"id": 4, "version": 5, "versions":' + ' [{"@id": "e", "downloads": 150, "version": "f"},' + ' {"@id": "g", "downloads": 250, "version": "h"}]}],' + ' "totalHits": 561}' + ) + assert obj.json == expected From 2df35716d71088774f7ee31ad6b89887ac3ed332 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Wed, 2 May 2018 11:58:32 -0700 Subject: [PATCH 24/34] Made _rename_keys a bit more robust --- src/pynuget/nuget_response.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pynuget/nuget_response.py b/src/pynuget/nuget_response.py index dc14eb2..fba4b00 100644 --- a/src/pynuget/nuget_response.py +++ b/src/pynuget/nuget_response.py @@ -4,6 +4,7 @@ import json +from pynuget import logger from pynuget.core import PyNuGetException @@ -16,7 +17,7 @@ def json(self): return encoder.encode(self) def _rename_keys(self, obj): - if hasattr(obj, '__dict__'): + if issubclass(type(obj), NuGetResponse): if hasattr(obj, 'json_key_map'): d = obj.__dict__ @@ -40,6 +41,9 @@ def _rename_keys(self, obj): # object is reached, _rename_keys() will be called again. return obj.__dict__ else: + logger.error("{} is not a subclass of NuGetResponse and is not" + " encodable as JSON (if it were, this function" + " would not have been called).") raise PyNuGetException("Doug, fix this") From 0939e2e608f3b5978d954aecd528c0974bd27c31 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Wed, 2 May 2018 11:59:56 -0700 Subject: [PATCH 25/34] Moved _rename_keys outside of the NuGetResponse class, since it's not dependent on it. --- src/pynuget/nuget_response.py | 66 +++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/pynuget/nuget_response.py b/src/pynuget/nuget_response.py index fba4b00..98bbe8b 100644 --- a/src/pynuget/nuget_response.py +++ b/src/pynuget/nuget_response.py @@ -8,44 +8,17 @@ from pynuget.core import PyNuGetException + + + class NuGetResponse(object): @property def json(self): """Return the object as JSON to send to NuGet""" - encoder = json.JSONEncoder(sort_keys=True, default=self._rename_keys) + encoder = json.JSONEncoder(sort_keys=True, default=_rename_keys) return encoder.encode(self) - def _rename_keys(self, obj): - if issubclass(type(obj), NuGetResponse): - if hasattr(obj, 'json_key_map'): - d = obj.__dict__ - - # wrap in list() because we're going to be modifying things - items = list(d.items()) - for key, value in items: - # Delete items with no value - if value is None: - del d[key] - continue - - # Map our python names to the NuGet API names. - if key in obj.json_key_map.keys(): - new_key = obj.json_key_map[key] - d[new_key] = d[key] - del d[key] - return d - else: - # Return the dict of the item we're processing. It will then - # continue on through the Encoder. When another unencodable - # object is reached, _rename_keys() will be called again. - return obj.__dict__ - else: - logger.error("{} is not a subclass of NuGetResponse and is not" - " encodable as JSON (if it were, this function" - " would not have been called).") - raise PyNuGetException("Doug, fix this") - class ServiceIndexResponse(NuGetResponse): @@ -142,3 +115,34 @@ class ContentResponse(NuGetResponse): class CatalogResponse(NuGetResponse): pass + + +def _rename_keys(obj): + if issubclass(type(obj), NuGetResponse): + if hasattr(obj, 'json_key_map'): + d = obj.__dict__ + + # wrap in list() because we're going to be modifying things + items = list(d.items()) + for key, value in items: + # Delete items with no value + if value is None: + del d[key] + continue + + # Map our python names to the NuGet API names. + if key in obj.json_key_map.keys(): + new_key = obj.json_key_map[key] + d[new_key] = d[key] + del d[key] + return d + else: + # Return the dict of the item we're processing. It will then + # continue on through the Encoder. When another unencodable + # object is reached, _rename_keys() will be called again. + return obj.__dict__ + else: + logger.error("{} is not a subclass of NuGetResponse and is not" + " encodable as JSON (if it were, this function" + " would not have been called).") + raise PyNuGetException("Doug, fix this") From ff3713b613db6de5a9778f5128f5953a2d76d76f Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Wed, 2 May 2018 15:22:15 -0700 Subject: [PATCH 26/34] Fill in unit tests for _rename_keys --- tests/test_nuget_response.py | 55 +++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/tests/test_nuget_response.py b/tests/test_nuget_response.py index 462b59c..5fedfce 100644 --- a/tests/test_nuget_response.py +++ b/tests/test_nuget_response.py @@ -1,12 +1,65 @@ # -*- coding: utf-8 -*- """ """ - +import json import pytest from pynuget import nuget_response as nr +@pytest.mark.temp +def test__rename_keys(): + + # Normal serializable objects should not even call this function, and so + # and error should be raised. + obj = {"a": "b"} + with pytest.raises(AttributeError): + nr._rename_keys(obj) + + # The function acceptes all items that inherit from Object. If they do + # not define `json_key_map` at the class level, then they are unchanged. + class Temp(object): + def __init__(self, a, b): + self.a = a + self.b = b + obj = Temp("a", 5) + result = nr._rename_keys(obj) + assert result == {"a": "a", "b": 5} + + # Any object that defines "json_key_map" should be modified + class Temp(object): + json_key_map = {"b": "@type"} + def __init__(self, a, b): + self.a = a + self.b = b + obj = Temp("a", 5) + result = nr._rename_keys(obj) + assert result == {"a": "a", "@type": 5} + + +@pytest.mark.temp +def test__rename_keys_encoder(): + + # Nested objects should work too, but only when we run the function + # via the encoder (since that handles the recursion). + class Temp(object): + json_key_map = {"b": "@type"} + def __init__(self, a, b): + self.a = a + self.b = b + + class Parent(object): + def __init__(self, a, b): + self.a = a + self.b = b + + obj = Parent(Temp("a", 5), 10) + encoded = json.JSONEncoder(sort_keys=True, + default=nr._rename_keys).encode(obj) + result = json.loads(encoded) + assert result == {"a": {"@type": 5, "a": "a"}, "b": 10} + + @pytest.mark.temp class TestNuGetResponse(): From 72ffde4e65aafe20404ce4f3abda4ceb24b00580 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Wed, 2 May 2018 15:24:00 -0700 Subject: [PATCH 27/34] Clean up _rename_keys - don't need to only act on NuGetReponse classes --- src/pynuget/nuget_response.py | 48 +++++++++++++++-------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/src/pynuget/nuget_response.py b/src/pynuget/nuget_response.py index 98bbe8b..0673d67 100644 --- a/src/pynuget/nuget_response.py +++ b/src/pynuget/nuget_response.py @@ -118,31 +118,25 @@ class CatalogResponse(NuGetResponse): def _rename_keys(obj): - if issubclass(type(obj), NuGetResponse): - if hasattr(obj, 'json_key_map'): - d = obj.__dict__ - - # wrap in list() because we're going to be modifying things - items = list(d.items()) - for key, value in items: - # Delete items with no value - if value is None: - del d[key] - continue - - # Map our python names to the NuGet API names. - if key in obj.json_key_map.keys(): - new_key = obj.json_key_map[key] - d[new_key] = d[key] - del d[key] - return d - else: - # Return the dict of the item we're processing. It will then - # continue on through the Encoder. When another unencodable - # object is reached, _rename_keys() will be called again. - return obj.__dict__ + if hasattr(obj, 'json_key_map'): + d = obj.__dict__ + + # wrap in list() because we're going to be modifying things + items = list(d.items()) + for key, value in items: + # Delete items with no value + if value is None: + del d[key] + continue + + # Map our python names to the NuGet API names. + if key in obj.json_key_map.keys(): + new_key = obj.json_key_map[key] + d[new_key] = d[key] + del d[key] + return d else: - logger.error("{} is not a subclass of NuGetResponse and is not" - " encodable as JSON (if it were, this function" - " would not have been called).") - raise PyNuGetException("Doug, fix this") + # Return the dict of the item we're processing. It will then + # continue on through the Encoder. When another unencodable + # object is reached, _rename_keys() will be called again. + return obj.__dict__ From eca42313a5e3625699c30331813a7bfb67a33053 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Wed, 2 May 2018 15:24:21 -0700 Subject: [PATCH 28/34] Add test for deleting 'None' values. --- tests/test_nuget_response.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_nuget_response.py b/tests/test_nuget_response.py index 5fedfce..817ebf2 100644 --- a/tests/test_nuget_response.py +++ b/tests/test_nuget_response.py @@ -36,6 +36,16 @@ def __init__(self, a, b): result = nr._rename_keys(obj) assert result == {"a": "a", "@type": 5} + # Items with a value of "None" get removed. + class Temp(object): + json_key_map = {"b": "@type"} + def __init__(self, a, b): + self.a = a + self.b = b + obj = Temp(None, 5) + result = nr._rename_keys(obj) + assert result == {"@type": 5} + @pytest.mark.temp def test__rename_keys_encoder(): From 0c41783db3f1d31d8a30be07227c57df9690e12e Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Thu, 3 May 2018 08:04:29 -0700 Subject: [PATCH 29/34] Removed class TestNuGetResponse --- tests/test_nuget_response.py | 101 +++++++++++++++++------------------ 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/tests/test_nuget_response.py b/tests/test_nuget_response.py index 817ebf2..6f9a596 100644 --- a/tests/test_nuget_response.py +++ b/tests/test_nuget_response.py @@ -70,57 +70,56 @@ def __init__(self, a, b): assert result == {"a": {"@type": 5, "a": "a"}, "b": 10} -@pytest.mark.temp -class TestNuGetResponse(): - - def test_to_json_ServiceIndexResourceResponse(self): - obj = nr.ServiceIndexResourceResponse( - url="aaa", - resource_type="bbb", - ) - - expected = '{"@id": "aaa", "@type": "bbb"}' - assert obj.json == expected - - obj = nr.ServiceIndexResourceResponse( - url="ccc", - resource_type="ddd", - comment="foo", - ) - - expected = '{"@id": "ccc", "@type": "ddd", "comment": "foo"}' - assert obj.json == expected - - def test_to_json_ServiceIndexResponse(self): - resources = [ - nr.ServiceIndexResourceResponse("a", "b"), - nr.ServiceIndexResourceResponse("c", "d", "e"), - ] - obj = nr.ServiceIndexResponse( - version="3.0.0", - resources=resources, - ) - - expected = ( - '{' - '"resources": [' - '{"@id": "a", "@type": "b"},' - ' {"@id": "c", "@type": "d", "comment": "e"}' - '], "version": "3.0.0"' - '}' - ) - assert obj.json == expected - - def test_json_SearchResponse(self): - - versions_a = [ - nr.SearchResultVersionResponse("a", "b", 100), - nr.SearchResultVersionResponse("c", "d", 50), - ] - versions_b = [ - nr.SearchResultVersionResponse("e", "f", 150), - nr.SearchResultVersionResponse("g", "h", 250), - ] +def test_to_json_ServiceIndexResourceResponse(self): + obj = nr.ServiceIndexResourceResponse( + url="aaa", + resource_type="bbb", + ) + + expected = '{"@id": "aaa", "@type": "bbb"}' + assert obj.json == expected + + obj = nr.ServiceIndexResourceResponse( + url="ccc", + resource_type="ddd", + comment="foo", + ) + + expected = '{"@id": "ccc", "@type": "ddd", "comment": "foo"}' + assert obj.json == expected + + +def test_to_json_ServiceIndexResponse(self): + resources = [ + nr.ServiceIndexResourceResponse("a", "b"), + nr.ServiceIndexResourceResponse("c", "d", "e"), + ] + obj = nr.ServiceIndexResponse( + version="3.0.0", + resources=resources, + ) + + expected = ( + '{' + '"resources": [' + '{"@id": "a", "@type": "b"},' + ' {"@id": "c", "@type": "d", "comment": "e"}' + '], "version": "3.0.0"' + '}' + ) + assert obj.json == expected + + +def test_json_SearchResponse(self): + + versions_a = [ + nr.SearchResultVersionResponse("a", "b", 100), + nr.SearchResultVersionResponse("c", "d", 50), + ] + versions_b = [ + nr.SearchResultVersionResponse("e", "f", 150), + nr.SearchResultVersionResponse("g", "h", 250), + ] data = [ nr.SearchResultResponse(1, 2, versions_a), From 7443eb0e3cb0a90d8077e340c57a2ad979d9eb57 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Thu, 3 May 2018 08:05:14 -0700 Subject: [PATCH 30/34] Fix names of tests: to_json no longer exists, was replaced with a property --- tests/test_nuget_response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_nuget_response.py b/tests/test_nuget_response.py index 6f9a596..8ecfc9a 100644 --- a/tests/test_nuget_response.py +++ b/tests/test_nuget_response.py @@ -70,7 +70,7 @@ def __init__(self, a, b): assert result == {"a": {"@type": 5, "a": "a"}, "b": 10} -def test_to_json_ServiceIndexResourceResponse(self): +def test_json_ServiceIndexResourceResponse(self): obj = nr.ServiceIndexResourceResponse( url="aaa", resource_type="bbb", @@ -89,7 +89,7 @@ def test_to_json_ServiceIndexResourceResponse(self): assert obj.json == expected -def test_to_json_ServiceIndexResponse(self): +def test_json_ServiceIndexResponse(self): resources = [ nr.ServiceIndexResourceResponse("a", "b"), nr.ServiceIndexResourceResponse("c", "d", "e"), From 4318cab343cc33d9724303fb144d2b06fdc7f7be Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Thu, 3 May 2018 08:06:38 -0700 Subject: [PATCH 31/34] Fix bug introduced by removing class --- tests/test_nuget_response.py | 46 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/test_nuget_response.py b/tests/test_nuget_response.py index 8ecfc9a..2732798 100644 --- a/tests/test_nuget_response.py +++ b/tests/test_nuget_response.py @@ -70,7 +70,7 @@ def __init__(self, a, b): assert result == {"a": {"@type": 5, "a": "a"}, "b": 10} -def test_json_ServiceIndexResourceResponse(self): +def test_json_ServiceIndexResourceResponse(): obj = nr.ServiceIndexResourceResponse( url="aaa", resource_type="bbb", @@ -89,7 +89,7 @@ def test_json_ServiceIndexResourceResponse(self): assert obj.json == expected -def test_json_ServiceIndexResponse(self): +def test_json_ServiceIndexResponse(): resources = [ nr.ServiceIndexResourceResponse("a", "b"), nr.ServiceIndexResourceResponse("c", "d", "e"), @@ -110,7 +110,7 @@ def test_json_ServiceIndexResponse(self): assert obj.json == expected -def test_json_SearchResponse(self): +def test_json_SearchResponse(): versions_a = [ nr.SearchResultVersionResponse("a", "b", 100), @@ -121,23 +121,23 @@ def test_json_SearchResponse(self): nr.SearchResultVersionResponse("g", "h", 250), ] - data = [ - nr.SearchResultResponse(1, 2, versions_a), - nr.SearchResultResponse(4, 5, versions_b), - ] - obj = nr.SearchResponse( - total_hits=561, - data=data, - ) - - expected = ( - '{"data":' - ' [{"id": 1, "version": 2, "versions":' - ' [{"@id": "a", "downloads": 100, "version": "b"},' - ' {"@id": "c", "downloads": 50, "version": "d"}]},' - ' {"id": 4, "version": 5, "versions":' - ' [{"@id": "e", "downloads": 150, "version": "f"},' - ' {"@id": "g", "downloads": 250, "version": "h"}]}],' - ' "totalHits": 561}' - ) - assert obj.json == expected + data = [ + nr.SearchResultResponse(1, 2, versions_a), + nr.SearchResultResponse(4, 5, versions_b), + ] + obj = nr.SearchResponse( + total_hits=561, + data=data, + ) + + expected = ( + '{"data":' + ' [{"id": 1, "version": 2, "versions":' + ' [{"@id": "a", "downloads": 100, "version": "b"},' + ' {"@id": "c", "downloads": 50, "version": "d"}]},' + ' {"id": 4, "version": 5, "versions":' + ' [{"@id": "e", "downloads": 150, "version": "f"},' + ' {"@id": "g", "downloads": 250, "version": "h"}]}],' + ' "totalHits": 561}' + ) + assert obj.json == expected From 3e8fbc6fc3f1d31fb2b029f2378e14efdc4bff6d Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Thu, 3 May 2018 08:08:30 -0700 Subject: [PATCH 32/34] Set values for all other kwargs in SearcResultResponse --- src/pynuget/nuget_response.py | 3 +++ tests/test_nuget_response.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pynuget/nuget_response.py b/src/pynuget/nuget_response.py index 0673d67..414021d 100644 --- a/src/pynuget/nuget_response.py +++ b/src/pynuget/nuget_response.py @@ -87,6 +87,9 @@ def __init__(self, id_, version, versions, **kwargs): self.version = version self.versions = versions + for key, value in kwargs.items(): + setattr(self, key, value) + class SearchResultVersionResponse(NuGetResponse): diff --git a/tests/test_nuget_response.py b/tests/test_nuget_response.py index 2732798..49da253 100644 --- a/tests/test_nuget_response.py +++ b/tests/test_nuget_response.py @@ -123,7 +123,7 @@ def test_json_SearchResponse(): data = [ nr.SearchResultResponse(1, 2, versions_a), - nr.SearchResultResponse(4, 5, versions_b), + nr.SearchResultResponse(4, 5, versions_b, project_url="http://no"), ] obj = nr.SearchResponse( total_hits=561, @@ -135,7 +135,7 @@ def test_json_SearchResponse(): ' [{"id": 1, "version": 2, "versions":' ' [{"@id": "a", "downloads": 100, "version": "b"},' ' {"@id": "c", "downloads": 50, "version": "d"}]},' - ' {"id": 4, "version": 5, "versions":' + ' {"id": 4, "projectUrl": "http://no", "version": 5, "versions":' ' [{"@id": "e", "downloads": 150, "version": "f"},' ' {"@id": "g", "downloads": 250, "version": "h"}]}],' ' "totalHits": 561}' From 18119a90d6f53181b2d77cc3fe4a37fb37d97070 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Thu, 3 May 2018 08:25:37 -0700 Subject: [PATCH 33/34] Add all NuGetResponse classes. --- src/pynuget/nuget_response.py | 296 +++++++++++++++++++++++++++++++++- 1 file changed, 291 insertions(+), 5 deletions(-) diff --git a/src/pynuget/nuget_response.py b/src/pynuget/nuget_response.py index 414021d..248f32f 100644 --- a/src/pynuget/nuget_response.py +++ b/src/pynuget/nuget_response.py @@ -108,16 +108,302 @@ def __init__(self, id_, version, downloads): self.downloads = downloads -class MetadataResponse(NuGetResponse): - pass +class MetadataIndexResponse(NuGetResponse): + + def __init__(self, count, items): + """ + count : int + items : array of RegistrationPage objects + """ + self.count = count + self.items = items + + +class MetadataIndexPage(NuGetResponse): + + json_key_map = { + "id_": "@id", + } + + def __inti__(self, id_, count, lower, upper, items=None, parent=None): + """ + id_ : string + count : int + lower : string + upper : string + items : array of MetadataIndexLeaf objects + parent : string + """ + self.id_ = id_ + self.count = count + self.lower = lower + self.upper = upper + self.items = items + self.parent = parent + + +class MetadataIndexLeaf(NuGetResponse): + + json_key_map = { + "id_": "@id", + "catalog_entry": "catalogEntry", + "pacakge_content": "pacakgeContent", + } + + def __init__(self, id_, catalog_entry, package_content): + """ + id_ : str + catalog_entry : CatalogEntry object + pacakge_content : str + """ + self.id_ = id_ + self.catalog_entry = catalog_entry + self.pacakge_content = package_content + + +class CatalogEntry(NuGetResponse): + + json_key_map = { + "id_": "@id", + "pkg_id": "id", + "dependency_groups": "dependencyGroups", + "icon_url": "iconUrl", + "license_url": "licenseUrl", + "min_client_version": "minClientVersion", + "project_url": "projectUrl", + "require_license_acceptance": " requireLicenseAcceptance", + } + + def __init__(self, id_, pkg_id, version, **kwargs): + """ + id_ : str + pkg_id : str + vesion : str + authors : str or list of str + dependency_groups : array of PackageDependencyGroup objects + description : str + icon_url : str + license_url : str + listed : bool + min_client_version : str + project_url : str + published : str (ISO 8601 timestamp) + require_license_acceptance : bool + summary : str + tags : str or list of str + title : str + """ + self.id_ = id_ + self.pkg_id = pkg_id + self.version = version + for key, value in kwargs.items(): + setattr(self, key, value) + + +class PackageDependencyGroup(NuGetResponse): + + json_key_map = { + "target_framework": "targetFramework", + } + + def __init__(self, target_framework, dependencies=None): + """ + target_framework : str + dependencies : array of PackageDependency objects + """ + self.target_framework = target_framework + self.dependencies = dependencies + + +class PackageDependency(NuGetResponse): + + json_key_map = { + "id_": "id", + "range_": "range", + } + + def __init__(self, id_, range_=None, registration=None): + """ + id_ : str + range_ : str (version range) + registration : str + """ + self.id_ = id_ + self.range_ = range_ + self.registration = registration + + +class RegistrationPageResponse(NuGetResponse): + + json_key_map = { + "id_": "@id", + } + + def __inti__(self, id_, count, lower, upper, items, parent): + """ + id_ : string + count : int + lower : string + upper : string + items : array of MetadataIndexLeaf objects + parent : string + """ + self.id_ = id_ + self.count = count + self.lower = lower + self.upper = upper + self.items = items + self.parent = parent + + +class RegistrationLeafResponse(NuGetResponse): + + json_key_map = { + "id_": "@id", + "catalog_entry": "catalogEntry", + "pacakge_content": "pacakgeContent", + } + + def __init__(self, id_, **kwargs): + """ + id_ : str + catalog_entry : str + listed : bool + pacakge_content : str + published : str (ISO 8601 timestamp) + registration : str + """ + self.id_ = id_ + for key, value in kwargs.items(): + setattr(self, key, value) class ContentResponse(NuGetResponse): - pass + def __init__(self, versions): + """ + versions : array of str + """ + self.versions = versions + + +class CatalogIndexResponse(NuGetResponse): + + json_key_map = { + "commit_id": "commitId", + "commit_timestamp": "commitTimeStamp", + } + + def __init__(self, commit_id, commit_timestamp, count, items): + """ + commit_id : str + commit_timestamp : str + count : int + itesm : list of CatalogIndexPage objects + """ + self.commit_id = commit_id + self.commit_timestamp = commit_timestamp + self.count = count + self.items = items + + +class CatalogIndexPage(NuGetResponse): + + json_key_map = { + "id_": "@id", + "commit_id": "commitId", + "commit_timestamp": "commitTimeStamp", + } + + def __init__(self, id_, commit_id, commit_timestamp, count): + """ + id_ : str + commit_id : str + commit_timestamp : str + count : int + """ + self.id_ = id_ + self.commit_id = commit_id + self.commit_timestamp = commit_timestamp + self.count = count + + +class CatalogPage(NuGetResponse): + + json_key_map = { + "commit_id": "commitId", + "commit_timestamp": "commitTimeStamp", + } + + def __init__(self, commit_id, commit_timestamp, count, items, parent): + """ + commit_id : str + commit_timestamp : str + count : int + items : list of CatalogItem objects + parent : str + """ + self.commit_id = commit_id + self.commit_timestamp = commit_timestamp + self.count = count + self.items = items + self.parent = parent + + +class CatalogItem(NuGetResponse): + + json_key_map = { + "id_": "@id", + "type_": "@type", + "commit_id": "commitId", + "commit_timestamp": "commitTimeStamp", + "nuget_id": "nuget:id", + "nuget_version": "nuget:version", + } + + def __init__(self, id_, type_, commit_id, commit_timestamp, + nuget_id, nuget_version): + """ + id_ : str + type_ : str + commit_id : str + commit_timestamp : str + nuget_id : str + nuget_version : str + """ + self.id_ = id_ + self.type_ = type_ + self.commit_id = commit_id + self.commit_timestamp = commit_timestamp + self.nuget_id = nuget_id + self.nuget_version = nuget_version -class CatalogResponse(NuGetResponse): - pass + +class CatalogLeaf(NuGetResponse): + + json_key_map = { + "type_": "@type", + "catalog_commit_id": "catalog:commitId", + "catalog_commit_timestamp": "catalog:commitTimeStamp", + "id_": "id", + } + + def __init__(self, type_, catalog_commit_id, catalog_timestamp, + id_, published, version): + """ + type_ : str or array of str + catalog_commit_id : str + catalog_commit_timestamp : str + id_ : str + published : str + version : str + """ + self.type_ = type_ + self.catalog_commit_id = catalog_commit_id + self.catalog_timestamp = catalog_timestamp + self.id_ = id_ + self.published = published + self.version = version def _rename_keys(obj): From 18d3618916dc75d161bd4a1e6acac7b7da2d5e6f Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Thu, 3 May 2018 09:18:16 -0700 Subject: [PATCH 34/34] Add test_json_ContentResponse --- tests/test_nuget_response.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_nuget_response.py b/tests/test_nuget_response.py index 49da253..0048856 100644 --- a/tests/test_nuget_response.py +++ b/tests/test_nuget_response.py @@ -141,3 +141,16 @@ def test_json_SearchResponse(): ' "totalHits": 561}' ) assert obj.json == expected + + +def test_json_ContentResponse(): + + versions = [ + "a", + "b", + "version1", + "0.1.5", + ] + obj = nr.ContentResponse(versions) + expected = '{"versions": ["a", "b", "version1", "0.1.5"]}' + assert obj.json == expected