diff --git a/monikerclient/exceptions.py b/monikerclient/exceptions.py new file mode 100644 index 00000000..09ebc14d --- /dev/null +++ b/monikerclient/exceptions.py @@ -0,0 +1,23 @@ +# Copyright 2012 Managed I.T. +# +# Author: Kiall Mac Innes <kiall@managedit.ie> +# +# Licensed under the Apache License, Version 2.0 (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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, 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. + + +class Base(Exception): + pass + + +class ResourceNotFound(Base): + pass diff --git a/monikerclient/utils.py b/monikerclient/utils.py new file mode 100644 index 00000000..dba207c6 --- /dev/null +++ b/monikerclient/utils.py @@ -0,0 +1,38 @@ +# Copyright 2012 Managed I.T. +# +# Author: Kiall Mac Innes <kiall@managedit.ie> +# +# Licensed under the Apache License, Version 2.0 (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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, 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. +import os +import pkg_resources +import json +from monikerclient import exceptions + + +def resource_string(*args): + if len(args) == 0: + raise ValueError() + + resource_path = os.path.join('resources', *args) + + if not pkg_resources.resource_exists('monikerclient', resource_path): + raise exceptions.ResourceNotFound('Could not find the requested ' + 'resource: %s' % resource_path) + + return pkg_resources.resource_string('monikerclient', resource_path) + + +def load_schema(version, name): + schema_string = resource_string('schemas', version, '%s.json' % name) + + return json.loads(schema_string) diff --git a/monikerclient/v1/__init__.py b/monikerclient/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/monikerclient/v1/client.py b/monikerclient/v1/client.py new file mode 100644 index 00000000..e40f4a97 --- /dev/null +++ b/monikerclient/v1/client.py @@ -0,0 +1,27 @@ +# Copyright 2012 Managed I.T. +# +# Author: Kiall Mac Innes <kiall@managedit.ie> +# +# Licensed under the Apache License, Version 2.0 (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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, 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. +from monikerclient.v1 import domains +from monikerclient.v1 import records +from monikerclient.v1 import servers + + +class Client(object): + """ Client for the Moniker v1 API """ + + def __init__(self): + self.domains = domains.Controller() + self.records = records.Controller() + self.servers = servers.Controller() diff --git a/monikerclient/v1/domains.py b/monikerclient/v1/domains.py new file mode 100644 index 00000000..c88de493 --- /dev/null +++ b/monikerclient/v1/domains.py @@ -0,0 +1,60 @@ +# Copyright 2012 Managed I.T. +# +# Author: Kiall Mac Innes <kiall@managedit.ie> +# +# Licensed under the Apache License, Version 2.0 (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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, 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. +from monikerclient import warlock +from monikerclient import utils + + +Domain = warlock.model_factory(utils.load_schema('v1', 'domain')) + + +class Controller(object): + def list(self): + """ + Retrieve a list of domains + + :returns: A list of :class:`Domain`s + """ + + def get(self, domain_id): + """ + Retrieve a domain + + :param domain_id: Domain Identifier + :returns: :class:`Domain` + """ + + def create(self, domain): + """ + Create a domain + + :param domain: A :class:`Domain` to create + :returns: :class:`Domain` + """ + + def update(self, domain): + """ + Update a domain + + :param domain: A :class:`Domain` to update + :returns: :class:`Domain` + """ + + def delete(self, domain): + """ + Delete a domain + + :param domain: A :class:`Domain`, or Domain Identifier to delete + """ diff --git a/monikerclient/v1/records.py b/monikerclient/v1/records.py new file mode 100644 index 00000000..d7b56a11 --- /dev/null +++ b/monikerclient/v1/records.py @@ -0,0 +1,60 @@ +# Copyright 2012 Managed I.T. +# +# Author: Kiall Mac Innes <kiall@managedit.ie> +# +# Licensed under the Apache License, Version 2.0 (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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, 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. +from monikerclient import warlock +from monikerclient import utils + + +Record = warlock.model_factory(utils.load_schema('v1', 'record')) + + +class Controller(object): + def list(self): + """ + Retrieve a list of records + + :returns: A list of :class:`Record`s + """ + + def get(self, record_id): + """ + Retrieve a record + + :param record_id: Record Identifier + :returns: :class:`Record` + """ + + def create(self, record): + """ + Create a record + + :param record: A :class:`Record` to create + :returns: :class:`Record` + """ + + def update(self, record): + """ + Update a record + + :param record: A :class:`Record` to update + :returns: :class:`Record` + """ + + def delete(self, record): + """ + Delete a record + + :param record: A :class:`Record`, or Record Identifier to delete + """ diff --git a/monikerclient/v1/servers.py b/monikerclient/v1/servers.py new file mode 100644 index 00000000..ec2923e5 --- /dev/null +++ b/monikerclient/v1/servers.py @@ -0,0 +1,60 @@ +# Copyright 2012 Managed I.T. +# +# Author: Kiall Mac Innes <kiall@managedit.ie> +# +# Licensed under the Apache License, Version 2.0 (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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, 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. +from monikerclient import warlock +from monikerclient import utils + + +Server = warlock.model_factory(utils.load_schema('v1', 'server')) + + +class Controller(object): + def list(self): + """ + Retrieve a list of servers + + :returns: A list of :class:`Server`s + """ + + def get(self, server_id): + """ + Retrieve a server + + :param server_id: Server Identifier + :returns: :class:`Server` + """ + + def create(self, server): + """ + Create a server + + :param server: A :class:`Server` to create + :returns: :class:`Server` + """ + + def update(self, server): + """ + Update a server + + :param server: A :class:`Server` to update + :returns: :class:`Server` + """ + + def delete(self, server): + """ + Delete a server + + :param server: A :class:`Server`, or Server Identifier to delete + """ diff --git a/monikerclient/warlock.py b/monikerclient/warlock.py new file mode 100644 index 00000000..e6db38ee --- /dev/null +++ b/monikerclient/warlock.py @@ -0,0 +1,124 @@ +# Copyright 2012 Brian Waldon +# +# Licensed under the Apache License, Version 2.0 (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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, 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. +# +# Code copied from Warlock, as warlock depends on jsonschema==0.2 +# Hopefully we can upstream the changes ASAP. +# +import copy +import jsonschema + + +class InvalidOperation(RuntimeError): + pass + + +class ValidationError(ValueError): + pass + + +def model_factory(schema): + """Generate a model class based on the provided JSON Schema + + :param schema: dict representing valid JSON schema + """ + schema = copy.deepcopy(schema) + + def validator(obj): + """Apply a JSON schema to an object""" + try: + jsonschema.validate(obj, schema) + except jsonschema.ValidationError: + raise ValidationError() + + class Model(dict): + """Self-validating model for arbitrary objects""" + + def __init__(self, *args, **kwargs): + d = dict(*args, **kwargs) + + # we overload setattr so set this manually + self.__dict__['validator'] = validator + try: + self.validator(d) + except ValidationError: + raise ValueError() + else: + dict.__init__(self, d) + + self.__dict__['changes'] = {} + + def __getattr__(self, key): + try: + return self.__getitem__(key) + except KeyError: + raise AttributeError(key) + + def __setitem__(self, key, value): + mutation = dict(self.items()) + mutation[key] = value + try: + self.validator(mutation) + except ValidationError: + raise InvalidOperation() + + dict.__setitem__(self, key, value) + + self.__dict__['changes'][key] = value + + def __setattr__(self, key, value): + self.__setitem__(key, value) + + def clear(self): + raise InvalidOperation() + + def pop(self, key, default=None): + raise InvalidOperation() + + def popitem(self): + raise InvalidOperation() + + def __delitem__(self, key): + raise InvalidOperation() + + # NOTE(termie): This is kind of the opposite of what copy usually does + def copy(self): + return copy.deepcopy(dict(self)) + + def update(self, other): + mutation = dict(self.items()) + mutation.update(other) + try: + self.validator(mutation) + except ValidationError: + raise InvalidOperation() + dict.update(self, other) + + def iteritems(self): + return copy.deepcopy(dict(self)).iteritems() + + def items(self): + return copy.deepcopy(dict(self)).items() + + def itervalues(self): + return copy.deepcopy(dict(self)).itervalues() + + def values(self): + return copy.deepcopy(dict(self)).values() + + @property + def changes(self): + return copy.deepcopy(self.__dict__['changes']) + + Model.__name__ = str(schema['title']) + return Model