diff --git a/.zuul.yaml b/.zuul.yaml index 17d0c86..d8a4190 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -19,24 +19,17 @@ parent: vexxhost-promote-docker-image vars: *atmosphere_images -- job: - name: atmosphere:linters:tox - parent: tox-linters - vars: - python_version: 3.7 - - project: check: jobs: - - atmosphere:linters:tox + - tox-linters - tox-py37 - atmosphere:image:build gate: jobs: - - atmosphere:linters:tox + - tox-linters - tox-py37 - atmosphere:image:upload promote: jobs: - - atmosphere:linters:tox - atmosphere:image:promote diff --git a/atmosphere/models.py b/atmosphere/models.py index a96ef81..ffca1dc 100644 --- a/atmosphere/models.py +++ b/atmosphere/models.py @@ -27,6 +27,7 @@ from flask_migrate import Migrate from sqlalchemy import exc from sqlalchemy.orm import exc as orm_exc from sqlalchemy.types import TypeDecorator +from sqlalchemy import or_ from atmosphere import exceptions @@ -105,6 +106,34 @@ class Resource(db.Model, GetOrCreateMixin): 'polymorphic_on': type } + @classmethod + def get_all_by_time_range(cls, start, end, project=None): + """Get all resources given a specific period.""" + query = cls.query.join(Period).filter( + # Resources must have started before the end + Period.started_at <= end, + # Resources must be still active or ended after start + or_( + Period.ended_at >= start, + Period.ended_at.is_(None) + ), + ) + + if project is not None: + query = query.filter(Resource.project == project) + + resources = query.all() + for resource in resources: + for period in resource.periods: + db.session.expunge(period) + if period.started_at <= start: + period.started_at = start + if period.ended_at is None or period.ended_at >= end: + period.ended_at = end + resource.periods = [p for p in resource.periods if p.seconds != 0] + + return resources + @classmethod def from_event(cls, event): """from_event""" @@ -249,7 +278,7 @@ class Period(db.Model): ended_at = db.Column(BigIntegerDateTime, index=True) spec_id = db.Column(db.Integer, db.ForeignKey('spec.id'), nullable=False) - spec = db.relationship("Spec") + spec = db.relationship("Spec", lazy='joined') @property def seconds(self): diff --git a/atmosphere/tests/unit/conftest.py b/atmosphere/tests/unit/conftest.py index 35c72db..acf2d47 100644 --- a/atmosphere/tests/unit/conftest.py +++ b/atmosphere/tests/unit/conftest.py @@ -42,6 +42,7 @@ def ignored_event(request): def app(): app = create_app() app.config['TESTING'] = True + app.config['SQLALCHEMY_ECHO'] = True app.register_blueprint(ingress.blueprint) return app diff --git a/atmosphere/tests/unit/test_models.py b/atmosphere/tests/unit/test_models.py index eda1da4..56ee890 100644 --- a/atmosphere/tests/unit/test_models.py +++ b/atmosphere/tests/unit/test_models.py @@ -64,6 +64,139 @@ class GetOrCreateTestMixin: class TestResource(GetOrCreateTestMixin): MODEL = models.Resource + def test_get_all_by_time_range_with_no_data(self): + start = datetime.datetime.now() + ended = start + relativedelta(hours=+1) + data = models.Resource.get_all_by_time_range(start, ended) + + assert len(data) == 0 + + def test_get_all_by_time_range_by_project(self): + event = fake.get_normalized_event() + resource = models.Resource.get_or_create(event) + + start = event['traits']['created_at'] - relativedelta(hours=+1) + ended = start + relativedelta(hours=+2) + + data = models.Resource.get_all_by_time_range(start, ended, + project="project") + assert len(data) == 0 + + data = models.Resource.get_all_by_time_range(start, ended, + project="fake-project") + assert len(data) == 1 + assert data[0].periods[0].seconds == 3600 + + def test_get_all_by_time_range_with_resource_ended_before_start(self): + event = fake.get_normalized_event() + event['traits']['deleted_at'] = event['traits']['created_at'] + \ + relativedelta(hours=+1) + + resource = models.Resource.get_or_create(event) + + start = event['traits']['deleted_at'] + relativedelta(hours=+1) + ended = start + relativedelta(hours=+1) + data = models.Resource.get_all_by_time_range(start, ended) + + assert len(data) == 0 + + def test_get_all_by_time_range_with_resource_started_after_end(self): + event = fake.get_normalized_event() + resource = models.Resource.get_or_create(event) + + ended = event['traits']['created_at'] - relativedelta(hours=+1) + start = ended - relativedelta(hours=+1) + data = models.Resource.get_all_by_time_range(start, ended) + + assert len(data) == 0 + + def test_get_all_by_time_range_with_active_resource_after_start(self): + event = fake.get_normalized_event() + resource = models.Resource.get_or_create(event) + + start = event['traits']['created_at'] - relativedelta(hours=+1) + ended = start + relativedelta(hours=+2) + data = models.Resource.get_all_by_time_range(start, ended) + + assert len(data) == 1 + assert data[0].periods[0].seconds == 3600 + + def test_get_all_by_time_range_with_active_resource_before_start(self): + event = fake.get_normalized_event() + resource = models.Resource.get_or_create(event) + + start = event['traits']['created_at'] + relativedelta(minutes=+30) + ended = start + relativedelta(minutes=+30) + data = models.Resource.get_all_by_time_range(start, ended) + + assert len(data) == 1 + assert data[0].periods[0].seconds == 1800 + + def test_get_all_by_time_range_with_active_resource_after_end(self): + event = fake.get_normalized_event() + event['traits']['deleted_at'] = event['traits']['created_at'] + \ + relativedelta(hours=+1) + + resource = models.Resource.get_or_create(event) + + start = event['traits']['deleted_at'] + relativedelta(hours=+1) + ended = start + relativedelta(hours=+2) + data = models.Resource.get_all_by_time_range(start, ended) + + assert len(data) == 0 + + def test_get_all_by_time_range_with_resource_inside_range(self): + event = fake.get_normalized_event() + event['traits']['deleted_at'] = event['traits']['created_at'] + \ + relativedelta(minutes=+15) + + resource = models.Resource.get_or_create(event) + + start = event['traits']['created_at'] - relativedelta(hours=+1) + ended = start + relativedelta(hours=+2) + data = models.Resource.get_all_by_time_range(start, ended) + + assert len(data) == 1 + assert data[0].periods[0].seconds == 900 + + def test_get_all_by_time_range_with_resource_with_multiple_periods(self): + event = fake.get_normalized_event() + event['traits']['created_at'] = event['traits']['created_at'] + \ + relativedelta(microseconds=0) + models.Resource.get_or_create(event) + + event['generated'] = event['traits']['created_at'] + \ + relativedelta(minutes=+15, microseconds=0) + event['traits']['instance_type'] = 'v2-standard-8' + models.Resource.get_or_create(event) + + start = event['traits']['created_at'] - relativedelta(hours=+1) + ended = start + relativedelta(hours=+2) + data = models.Resource.get_all_by_time_range(start, ended) + + assert len(data) == 1 + assert data[0].periods[0].seconds == 900 + assert data[0].periods[1].seconds == 2700 + + def test_get_all_by_time_range_with_resource_with_one_active_period(self): + event = fake.get_normalized_event() + event['traits']['created_at'] = event['traits']['created_at'] + \ + relativedelta(microseconds=0) + models.Resource.get_or_create(event) + + event['generated'] = event['traits']['created_at'] + \ + relativedelta(minutes=+15, microseconds=0) + event['traits']['instance_type'] = 'v2-standard-8' + models.Resource.get_or_create(event) + + start = event['traits']['created_at'] + relativedelta(minutes=+15) + ended = start + relativedelta(minutes=+45) + data = models.Resource.get_all_by_time_range(start, ended) + + assert len(data) == 1 + assert len(data[0].periods) == 1 + assert data[0].periods[0].seconds == 2700 + def test_from_event(self): event = fake.get_normalized_event() resource = models.Resource.from_event(event)