Skip to content

Commit

Permalink
feat: add Observers and Observations models
Browse files Browse the repository at this point in the history
Signed-off-by: Mike Fiedler <miketheman@gmail.com>
  • Loading branch information
miketheman committed Nov 6, 2023
1 parent 179040a commit 9d5c3ee
Show file tree
Hide file tree
Showing 9 changed files with 603 additions and 3 deletions.
20 changes: 20 additions & 0 deletions tests/common/db/observations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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 warehouse.observations.models import Observer

from .base import WarehouseFactory


class ObserverFactory(WarehouseFactory):
class Meta:
model = Observer
13 changes: 13 additions & 0 deletions tests/common/db/packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import faker
import packaging.utils

from warehouse.observations.models import ObservationKind
from warehouse.packaging.models import (
Dependency,
DependencyKind,
Expand Down Expand Up @@ -56,6 +57,18 @@ class Meta:
source = factory.SubFactory(ProjectFactory)


class ProjectObservationFactory(WarehouseFactory):
class Meta:
model = Project.Observation

kind = factory.Faker(
"random_element", elements=[kind.value[1] for kind in ObservationKind]
)
payload = factory.Faker("json")
# TODO: add `observer` field
summary = factory.Faker("paragraph")


class DescriptionFactory(WarehouseFactory):
class Meta:
model = Description
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/observations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 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.
102 changes: 102 additions & 0 deletions tests/unit/observations/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 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 datetime import datetime
from uuid import UUID

from warehouse.observations.models import ObservationKind

from ...common.db.accounts import UserFactory
from ...common.db.observations import ObserverFactory
from ...common.db.packaging import ProjectFactory, ReleaseFactory


def test_observer(db_session):
observer = ObserverFactory.create()

assert isinstance(observer.id, UUID)
assert isinstance(observer.created, datetime)
assert observer.parent is None


def test_user_observer_relationship(db_session):
observer = ObserverFactory.create()
user = UserFactory.create(observer=observer)

assert user.observer == observer
assert observer.parent == user


def test_observer_observations_relationship(db_request):
user = UserFactory.create()
db_request.user = user
project = ProjectFactory.create()

project.record_observation(
request=db_request,
kind=ObservationKind.SomethingElse,
summary="Project Observation",
payload={},
observer=user,
)

assert len(project.observations) == 1
observation = project.observations[0]
assert observation.observer.parent == user
assert str(observation) == "<ProjectObservation something_else>"
assert observation.kind_display == "Something Else"


def test_observer_created_from_user_when_observation_made(db_request):
user = UserFactory.create()
db_request.user = user
project = ProjectFactory.create()

project.record_observation(
request=db_request,
kind=ObservationKind.SomethingElse,
summary="Project Observation",
payload={},
observer=user,
)

assert len(project.observations) == 1
observation = project.observations[0]
assert observation.observer.parent == user
assert str(observation) == "<ProjectObservation something_else>"


def test_user_observations_relationship(db_request):
user = UserFactory.create()
db_request.user = user
project = ProjectFactory.create()
release = ReleaseFactory.create(project=project)

project.record_observation(
request=db_request,
kind=ObservationKind.SomethingElse,
summary="Project Observation",
payload={},
observer=user,
)
release.record_observation(
request=db_request,
kind=ObservationKind.SomethingElse,
summary="Release Observation",
payload={},
observer=user,
)

db_request.db.flush() # so Observer is created

assert len(user.observer.observations) == 2
assert len(user.observations) == 2
3 changes: 2 additions & 1 deletion warehouse/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

from warehouse import db
from warehouse.events.models import HasEvents
from warehouse.observations.models import HasObserversMixin
from warehouse.sitemap.models import SitemapMixin
from warehouse.utils.attrs import make_repr
from warehouse.utils.db.types import TZDateTime, bool_false, datetime_now
Expand Down Expand Up @@ -68,7 +69,7 @@ class DisableReason(enum.Enum):
AccountFrozen = "account frozen"


class User(SitemapMixin, HasEvents, db.Model):
class User(SitemapMixin, HasObserversMixin, HasEvents, db.Model):
__tablename__ = "users"
__table_args__ = (
CheckConstraint("length(username) <= 50", name="users_valid_username_length"),
Expand Down
214 changes: 214 additions & 0 deletions warehouse/migrations/versions/4297620f7b41_observations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# 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.
"""
Observations
Revision ID: 4297620f7b41
Revises: 186f076eb60b
Create Date: 2023-10-31 21:56:51.280480
"""

import sqlalchemy as sa

from alembic import op
from sqlalchemy.dialects import postgresql

revision = "4297620f7b41"
down_revision = "186f076eb60b"

# Note: It is VERY important to ensure that a migration does not lock for a
# long period of time and to ensure that each individual migration does
# not break compatibility with the *previous* version of the code base.
# This is because the migrations will be ran automatically as part of the
# deployment process, but while the previous version of the code is still
# up and running. Thus backwards incompatible changes must be broken up
# over multiple migrations inside of multiple pull requests in order to
# phase them in over multiple deploys.
#
# By default, migrations cannot wait more than 4s on acquiring a lock
# and each individual statement cannot take more than 5s. This helps
# prevent situations where a slow migration takes the entire site down.
#
# If you need to increase this timeout for a migration, you can do so
# by adding:
#
# op.execute("SET statement_timeout = 5000")
# op.execute("SET lock_timeout = 4000")
#
# To whatever values are reasonable for this migration as part of your
# migration.


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"observer_association",
sa.Column(
"discriminator",
sa.String(),
nullable=False,
comment="The type of the parent",
),
sa.Column(
"id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"observers",
sa.Column(
"created", sa.DateTime(), server_default=sa.text("now()"), nullable=False
),
sa.Column("_association_id", sa.UUID(), nullable=True),
sa.Column(
"id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False
),
sa.ForeignKeyConstraint(
["_association_id"],
["observer_association.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"project_observations",
sa.Column(
"related_id",
sa.UUID(),
nullable=False,
comment="The ID of the related model",
),
sa.Column(
"observer_id",
sa.UUID(),
nullable=False,
comment="ID of the Observer who created the Observation",
),
sa.Column(
"created",
sa.DateTime(),
server_default=sa.text("now()"),
nullable=False,
comment="The time the observation was created",
),
sa.Column(
"kind", sa.String(), nullable=False, comment="The kind of observation"
),
sa.Column(
"summary",
sa.String(),
nullable=False,
comment="A short summary of the observation",
),
sa.Column(
"payload",
postgresql.JSONB(astext_type=sa.Text()),
nullable=False,
comment="The observation payload we received",
),
sa.Column(
"id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False
),
sa.ForeignKeyConstraint(
["observer_id"],
["observers.id"],
),
sa.ForeignKeyConstraint(
["related_id"],
["projects.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_project_observations_related_id"),
"project_observations",
["related_id"],
unique=False,
)
op.create_table(
"release_observations",
sa.Column(
"related_id",
sa.UUID(),
nullable=False,
comment="The ID of the related model",
),
sa.Column(
"observer_id",
sa.UUID(),
nullable=False,
comment="ID of the Observer who created the Observation",
),
sa.Column(
"created",
sa.DateTime(),
server_default=sa.text("now()"),
nullable=False,
comment="The time the observation was created",
),
sa.Column(
"kind", sa.String(), nullable=False, comment="The kind of observation"
),
sa.Column(
"summary",
sa.String(),
nullable=False,
comment="A short summary of the observation",
),
sa.Column(
"payload",
postgresql.JSONB(astext_type=sa.Text()),
nullable=False,
comment="The observation payload we received",
),
sa.Column(
"id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False
),
sa.ForeignKeyConstraint(
["observer_id"],
["observers.id"],
),
sa.ForeignKeyConstraint(
["related_id"],
["releases.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_release_observations_related_id"),
"release_observations",
["related_id"],
unique=False,
)
op.add_column(
"users", sa.Column("observer_association_id", sa.UUID(), nullable=True)
)
op.create_foreign_key(
None, "users", "observer_association", ["observer_association_id"], ["id"]
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "users", type_="foreignkey")
op.drop_column("users", "observer_association_id")
op.drop_index(
op.f("ix_release_observations_related_id"), table_name="release_observations"
)
op.drop_table("release_observations")
op.drop_index(
op.f("ix_project_observations_related_id"), table_name="project_observations"
)
op.drop_table("project_observations")
op.drop_table("observers")
op.drop_table("observer_association")
# ### end Alembic commands ###
Loading

0 comments on commit 9d5c3ee

Please sign in to comment.