From 4b048f0f98a25ef3f00d79df156ef48d64b38400 Mon Sep 17 00:00:00 2001 From: Morten Lied Johansen Date: Mon, 6 Nov 2017 15:52:44 +0100 Subject: [PATCH 1/3] DOCD-1319 - Support Time fields, using datetime.datetime --- k8s/fields.py | 11 +++- setup.py | 3 +- tests/k8s/test_fields.py | 126 +++++++++++++++++++++++++++++++++++++++ tests/k8s/test_model.py | 94 ++--------------------------- 4 files changed, 141 insertions(+), 93 deletions(-) create mode 100644 tests/k8s/test_fields.py diff --git a/k8s/fields.py b/k8s/fields.py index 1c839ac..134e6cf 100644 --- a/k8s/fields.py +++ b/k8s/fields.py @@ -2,6 +2,10 @@ # -*- coding: utf-8 from __future__ import absolute_import +from datetime import datetime + +import pyrfc3339 + class Field(object): """Generic field on a k8s model""" @@ -56,8 +60,7 @@ def default_value(self): return self.type(new=False) return self._default_value - @staticmethod - def _as_dict(value): + def _as_dict(self, value): try: return value.as_dict() except AttributeError: @@ -67,6 +70,8 @@ def _as_dict(value): if isinstance(value, dict): d = {k: v for k, v in value.items() if v is not None} return d if d else None + elif datetime in (self.type, self.alt_type) and isinstance(value, datetime): + return pyrfc3339.generate(value, accept_naive=True) else: return value @@ -78,6 +83,8 @@ def _from_dict(self, value): except AttributeError: if isinstance(value, self.type) or (self.alt_type and isinstance(value, self.alt_type)): return value + if self.type is datetime: + return pyrfc3339.parse(value) return self.type(value) def __repr__(self): diff --git a/setup.py b/setup.py index eab7ed4..5e54cb2 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,8 @@ GENERIC_REQ = [ "six == 1.10.0", - "requests == 2.13.0" + "requests == 2.13.0", + "pyrfc3339 == 1.0" ] CODE_QUALITY_REQ = [ diff --git a/tests/k8s/test_fields.py b/tests/k8s/test_fields.py new file mode 100644 index 0000000..89eb588 --- /dev/null +++ b/tests/k8s/test_fields.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 +from datetime import datetime + +import pytest +import pytz +import six + +from k8s import config +from k8s.base import Model +from k8s.fields import Field, ListField, OnceField, ReadOnlyField, RequiredField +from k8s.models.common import ObjectMeta + + +class ModelTest(Model): + class Meta: + pass + + metadata = Field(ObjectMeta) + field = Field(int) + list_field = ListField(int) + once_field = OnceField(int) + read_only_field = ReadOnlyField(int) + alt_type_field = Field(int, alt_type=six.text_type) + dict_field = Field(dict) + _exec = Field(int) + time_field = Field(datetime) + + +class TestFields(object): + @pytest.fixture(autouse=True) + def set_config_debug(self, monkeypatch): + monkeypatch.setattr(config, "debug", True) + + @pytest.mark.parametrize("field_name,initial_value,other_value", ( + ("field", 1, 2), + ("list_field", [1], [1, 2]), + ("once_field", 1, 2), + ("_exec", 1, 2) + )) + def test_field_new(self, field_name, initial_value, other_value): + kwargs = {"new": True, field_name: initial_value} + model = ModelTest(**kwargs) + assert getattr(model, field_name) == initial_value + setattr(model, field_name, other_value) + assert getattr(model, field_name) == other_value + + @pytest.mark.parametrize("field_name,initial_value,other_value", ( + ("field", 1, 2), + ("list_field", [1], [1, 2]), + )) + def test_field_old(self, field_name, initial_value, other_value): + model = ModelTest.from_dict({field_name: initial_value}) + assert getattr(model, field_name) == initial_value + setattr(model, field_name, other_value) + assert getattr(model, field_name) == other_value + + def test_once_field_old(self): + model = ModelTest.from_dict({"once_field": 1}) + assert model.once_field == 1 + model.once_field = 2 + assert model.once_field == 1 + + def test_exec_field_old(self): + model = ModelTest.from_dict({"exec": 1}) + assert model._exec == 1 + model._exec = 2 + assert model._exec == 2 + assert model.as_dict()[u"exec"] == 2 + + def test_read_only_field_new(self): + model = ModelTest(new=True, read_only_field=1) + assert model.read_only_field is None + model.read_only_field = 2 + assert model.read_only_field is None + + def test_read_only_field_old(self): + model = ModelTest.from_dict({"read_only_field": 1}) + assert model.read_only_field == 1 + model.read_only_field = 2 + assert model.read_only_field == 1 + + @pytest.mark.parametrize("value,modifier", [ + (1, lambda x: x + 1), + (u"string", lambda x: x.upper()) + ]) + def test_alt_type_field(self, value, modifier): + model = ModelTest.from_dict({"alt_type_field": value}) + assert model.alt_type_field == value + assert model.as_dict()[u"alt_type_field"] == value + model.alt_type_field = modifier(value) + assert model.alt_type_field == modifier(value) + + @pytest.mark.parametrize("input,dt", ( + ("2009-01-01T17:59:59Z", datetime(2009, 1, 1, 17, 59, 59, tzinfo=pytz.UTC)), + ("2009-01-01T17:59:59+01:00", datetime(2009, 1, 1, 16, 59, 59, tzinfo=pytz.UTC)), + )) + def test_time_field_from_dict(self, input, dt): + model = ModelTest.from_dict({"time_field": input}) + assert isinstance(model.time_field, datetime) + assert model.time_field == dt + + def test_time_field_as_dict(self): + model = ModelTest(time_field=datetime(2009, 1, 1, 17, 59, 59, tzinfo=pytz.UTC)) + d = model.as_dict() + assert d["time_field"] == "2009-01-01T17:59:59Z" + + +class RequiredFieldTest(Model): + required_field = RequiredField(int) + field = Field(int, 100) + + +@pytest.mark.usefixtures("logger") +class TestRequiredField(object): + @pytest.mark.parametrize("kwargs", [ + {"required_field": 1, "field": 2}, + {"required_field": 1}, + ]) + def test_create_with_fields(self, kwargs): + instance = RequiredFieldTest(new=True, **kwargs) + for key, value in kwargs.items(): + assert getattr(instance, key) == value + + def test_create_fails_when_field_missing(self): + with pytest.raises(TypeError): + RequiredFieldTest(new=True, field=1) diff --git a/tests/k8s/test_model.py b/tests/k8s/test_model.py index c01474a..61038a9 100644 --- a/tests/k8s/test_model.py +++ b/tests/k8s/test_model.py @@ -2,13 +2,13 @@ # -*- coding: utf-8 import pytest import six -from k8s import config +from mock import create_autospec +from requests import Response + from k8s.base import Model from k8s.client import Client -from k8s.fields import Field, ReadOnlyField, OnceField, ListField, RequiredField +from k8s.fields import Field, ListField, OnceField, ReadOnlyField from k8s.models.common import ObjectMeta -from mock import create_autospec -from requests import Response class ModelTest(Model): @@ -25,92 +25,6 @@ class Meta: _exec = Field(int) -class TestFields(object): - @pytest.fixture(autouse=True) - def set_config_debug(self, monkeypatch): - monkeypatch.setattr(config, "debug", True) - - @pytest.mark.parametrize("field_name,initial_value,other_value", ( - ("field", 1, 2), - ("list_field", [1], [1, 2]), - ("once_field", 1, 2), - ("_exec", 1, 2) - )) - def test_field_new(self, field_name, initial_value, other_value): - kwargs = {"new": True, field_name: initial_value} - model = ModelTest(**kwargs) - assert getattr(model, field_name) == initial_value - setattr(model, field_name, other_value) - assert getattr(model, field_name) == other_value - - @pytest.mark.parametrize("field_name,initial_value,other_value", ( - ("field", 1, 2), - ("list_field", [1], [1, 2]), - )) - def test_field_old(self, field_name, initial_value, other_value): - model = ModelTest.from_dict({field_name: initial_value}) - assert getattr(model, field_name) == initial_value - setattr(model, field_name, other_value) - assert getattr(model, field_name) == other_value - - def test_once_field_old(self): - model = ModelTest.from_dict({"once_field": 1}) - assert model.once_field == 1 - model.once_field = 2 - assert model.once_field == 1 - - def test_exec_field_old(self): - model = ModelTest.from_dict({"exec": 1}) - assert model._exec == 1 - model._exec = 2 - assert model._exec == 2 - assert model.as_dict()[u"exec"] == 2 - - def test_read_only_field_new(self): - model = ModelTest(new=True, read_only_field=1) - assert model.read_only_field is None - model.read_only_field = 2 - assert model.read_only_field is None - - def test_read_only_field_old(self): - model = ModelTest.from_dict({"read_only_field": 1}) - assert model.read_only_field == 1 - model.read_only_field = 2 - assert model.read_only_field == 1 - - @pytest.mark.parametrize("value,modifier", [ - (1, lambda x: x + 1), - (u"string", lambda x: x.upper()) - ]) - def test_alt_type_field(self, value, modifier): - model = ModelTest.from_dict({"alt_type_field": value}) - assert model.alt_type_field == value - assert model.as_dict()[u"alt_type_field"] == value - model.alt_type_field = modifier(value) - assert model.alt_type_field == modifier(value) - - -class RequiredFieldTest(Model): - required_field = RequiredField(int) - field = Field(int, 100) - - -@pytest.mark.usefixtures("logger") -class TestRequiredField(object): - @pytest.mark.parametrize("kwargs", [ - {"required_field": 1, "field": 2}, - {"required_field": 1}, - ]) - def test_create_with_fields(self, kwargs): - instance = RequiredFieldTest(new=True, **kwargs) - for key, value in kwargs.items(): - assert getattr(instance, key) == value - - def test_create_fails_when_field_missing(self): - with pytest.raises(TypeError): - RequiredFieldTest(new=True, field=1) - - @pytest.mark.usefixtures("logger") class TestModel(object): @pytest.fixture() From e53446528520a7f0c575ca215e55b1a78dd4b424 Mon Sep 17 00:00:00 2001 From: Morten Lied Johansen Date: Mon, 6 Nov 2017 20:14:03 +0100 Subject: [PATCH 2/3] Make future diffs easier to read --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 5e54cb2..45f46ca 100644 --- a/setup.py +++ b/setup.py @@ -7,11 +7,11 @@ GENERIC_REQ = [ "six == 1.10.0", "requests == 2.13.0", - "pyrfc3339 == 1.0" + "pyrfc3339 == 1.0", ] CODE_QUALITY_REQ = [ - 'prospector' + 'prospector', ] TESTS_REQ = [ @@ -23,7 +23,7 @@ 'pytest-cov', 'pytest-helpers-namespace', 'pytest >= 3.0', - 'gitpython' + 'gitpython', ] From 072717ee3c4971bd912865607f19a9a3439e876e Mon Sep 17 00:00:00 2001 From: Morten Lied Johansen Date: Tue, 7 Nov 2017 10:31:07 +0100 Subject: [PATCH 3/3] Updated documentation --- docs/source/developer.rst | 2 +- docs/source/modules/k8s.models.configmap.rst | 7 +++++++ docs/source/modules/k8s.models.resourcequota.rst | 7 +++++++ docs/source/modules/k8s.models.rst | 2 ++ 4 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docs/source/modules/k8s.models.configmap.rst create mode 100644 docs/source/modules/k8s.models.resourcequota.rst diff --git a/docs/source/developer.rst b/docs/source/developer.rst index 0718ce7..50c69ec 100644 --- a/docs/source/developer.rst +++ b/docs/source/developer.rst @@ -42,7 +42,7 @@ Use this class... ...when The :py:class:`~k8s.fields.Field` class takes three parameters: type - The type of value this field contains. Can be simple types (int, bool etc), or subclasses of :py:class:`~k8s.base.Model`. + The type of value this field contains. Can be simple types (int, bool etc), :py:class:`datetime.datetime` or subclasses of :py:class:`~k8s.base.Model`. default_value The field is set to this value when an instance of the class is created. The default default is ``None``. diff --git a/docs/source/modules/k8s.models.configmap.rst b/docs/source/modules/k8s.models.configmap.rst new file mode 100644 index 0000000..4b341dc --- /dev/null +++ b/docs/source/modules/k8s.models.configmap.rst @@ -0,0 +1,7 @@ +k8s\.models\.configmap module +============================= + +.. automodule:: k8s.models.configmap + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/modules/k8s.models.resourcequota.rst b/docs/source/modules/k8s.models.resourcequota.rst new file mode 100644 index 0000000..0fc0dbf --- /dev/null +++ b/docs/source/modules/k8s.models.resourcequota.rst @@ -0,0 +1,7 @@ +k8s\.models\.resourcequota module +================================= + +.. automodule:: k8s.models.resourcequota + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/modules/k8s.models.rst b/docs/source/modules/k8s.models.rst index 8dc6a28..a251348 100644 --- a/docs/source/modules/k8s.models.rst +++ b/docs/source/modules/k8s.models.rst @@ -13,10 +13,12 @@ Submodules k8s.models.autoscaler k8s.models.common + k8s.models.configmap k8s.models.deployment k8s.models.ingress k8s.models.pod k8s.models.replication_controller + k8s.models.resourcequota k8s.models.service k8s.models.third_party_resource