class Build(ProjectBoundMixin, StandardAttributes, db.Model): """ A single build linked to a source. Each Build contains many Jobs. """ source_id = db.Column( GUID, db.ForeignKey('source.id', ondelete='CASCADE'), nullable=False, index=True ) number = db.Column(db.Integer, nullable=False) label = db.Column(db.String, nullable=False) status = db.Column(Enum(Status), nullable=False, default=Status.unknown) result = db.Column(Enum(Result), nullable=False, default=Result.unknown) date_started = db.Column(db.TIMESTAMP(timezone=True), nullable=True) date_finished = db.Column(db.TIMESTAMP(timezone=True), nullable=True) data = db.Column(JSONEncodedDict, nullable=True) provider = db.Column(db.String, nullable=True) external_id = db.Column(db.String(64), nullable=True) source = db.relationship('Source') stats = db.relationship( 'ItemStat', foreign_keys='[ItemStat.item_id]', primaryjoin='ItemStat.item_id == Build.id', lazy='subquery', viewonly=True, uselist=True ) __tablename__ = 'build' __table_args__ = ( db.UniqueConstraint('project_id', 'number', name='unq_build_number'), db.UniqueConstraint('project_id', 'provider', 'external_id', name='unq_build_provider') ) __repr__ = model_repr('number', 'status', 'result')
class Artifact(RepositoryBoundMixin, StandardAttributes, db.Model): job_id = db.Column(GUID, db.ForeignKey( 'job.id', ondelete='CASCADE'), nullable=False) testcase_id = db.Column(GUID, db.ForeignKey( 'testcase.id', ondelete='CASCADE'), nullable=True) name = db.Column(db.String(length=256), nullable=False) type = db.Column(db.String(length=64), nullable=True) file = db.Column( File(path='artifacts'), nullable=False, # TODO(dcramer): this is super hacky but not sure a better way to # do it with SQLAlchemy default=lambda: FileData({}, default_path='artifacts') ) status = db.Column(Enum(Status), nullable=False, default=Status.unknown) date_started = db.Column(db.TIMESTAMP(timezone=True), nullable=True) date_updated = db.Column(db.TIMESTAMP( timezone=True), nullable=True, onupdate=timezone.now) date_finished = db.Column(db.TIMESTAMP(timezone=True), nullable=True) job = db.relationship('Job', innerjoin=True, uselist=False) testcase = db.relationship('TestCase', uselist=False) __tablename__ = 'artifact' def save_base64_content(self, base64): content = b64decode(base64) self.file.save( BytesIO( content), '{0}/{1}_{2}'.format(self.job_id.hex, self.id.hex, self.name) )
class Repository(OrganizationBoundMixin, StandardAttributes, db.Model): """ Represents a single repository. """ query_class = RepositoryAccessBoundQuery provider = db.Column(StrEnum(RepositoryProvider), default=RepositoryProvider.native, nullable=False) external_id = db.Column(db.String(64)) url = db.Column(db.String(200), nullable=False) backend = db.Column(Enum(RepositoryBackend), default=RepositoryBackend.unknown, nullable=False) status = db.Column(Enum(RepositoryStatus), default=RepositoryStatus.inactive, nullable=False) data = db.Column(JSONEncodedDict, nullable=True) last_update = db.Column(db.TIMESTAMP(timezone=True), nullable=True) last_update_attempt = db.Column(db.TIMESTAMP(timezone=True), nullable=True) options = db.relationship( 'ItemOption', foreign_keys='[ItemOption.item_id]', primaryjoin='ItemOption.item_id == Repository.id', lazy='subquery', viewonly=True, uselist=True) __tablename__ = 'repository' __table_args__ = (db.UniqueConstraint('organization_id', 'provider', 'external_id', name='unq_external_id'), ) __repr__ = model_repr('url', 'provider', 'external_id') def get_vcs(self): from zeus.models import ItemOption from zeus.vcs.git import GitVcs options = dict( db.session.query(ItemOption.name, ItemOption.value).filter( ItemOption.item_id == self.id, ItemOption.name.in_([ 'auth.username', ]))) kwargs = { 'path': os.path.join(current_app.config['REPO_ROOT'], self.id.hex), 'url': self.url, 'username': options.get('auth.username'), } if self.backend == RepositoryBackend.git: return GitVcs(**kwargs) else: raise NotImplementedError('Invalid backend: {}'.format( self.backend))
class Job(RepositoryBoundMixin, StandardAttributes, db.Model): """ A single job, which is the actual execution unit for a build. """ id = db.Column(GUID, primary_key=True, default=GUID.default_value) build_id = db.Column(GUID, db.ForeignKey("build.id", ondelete="CASCADE"), nullable=False, index=True) number = db.Column(db.Integer, nullable=False) label = db.Column(db.String, nullable=True) status = db.Column(Enum(Status), nullable=False, default=Status.unknown) result = db.Column(Enum(Result), nullable=False, default=Result.unknown) allow_failure = db.Column(db.Boolean, nullable=False, default=False, server_default="0") date_started = db.Column(db.TIMESTAMP(timezone=True), nullable=True) date_updated = db.Column(db.TIMESTAMP(timezone=True), nullable=True, onupdate=timezone.now) date_finished = db.Column(db.TIMESTAMP(timezone=True), nullable=True) data = db.Column(JSONEncodedDict, nullable=True) provider = db.Column(db.String, nullable=True) external_id = db.Column(db.String(64), nullable=True) url = db.Column(db.String, nullable=True) build = db.relationship("Build", backref=db.backref("jobs", order_by="Job.date_created"), innerjoin=True) stats = db.relationship( "ItemStat", foreign_keys="[ItemStat.item_id]", primaryjoin="ItemStat.item_id == Job.id", viewonly=True, uselist=True, ) failures = db.relationship( "FailureReason", foreign_keys="[FailureReason.job_id]", primaryjoin="FailureReason.job_id == Job.id", viewonly=True, uselist=True, ) __tablename__ = "job" __table_args__ = ( db.UniqueConstraint("build_id", "number", name="unq_job_number"), db.UniqueConstraint("build_id", "provider", "external_id", name="unq_job_provider"), ) __repr__ = model_repr("build_id", "number", "status", "result")
class Revision(db.Model): # XXX(dcramer): the primary_key doesnt include repo_id at the moment, which is wrong, but # we need to deal w/ that in a followup change repository_id = db.Column( GUID, db.ForeignKey("repository.id", ondelete="CASCADE"), primary_key=True ) sha = db.Column(db.String(40), primary_key=True) author_id = db.Column( GUID, db.ForeignKey("author.id", ondelete="SET NULL"), index=True, nullable=True ) committer_id = db.Column( GUID, db.ForeignKey("author.id", ondelete="SET NULL"), index=True, nullable=True ) message = db.Column(db.Text, nullable=True) parents = db.Column(ARRAY(db.String(40)), nullable=True) # TODO: remove this column, we dont use it and its wrong branches = db.Column(ARRAY(db.String(128)), nullable=True) date_created = db.Column( db.TIMESTAMP(timezone=True), default=timezone.now, server_default=func.now(), nullable=False, ) date_committed = db.Column( db.TIMESTAMP(timezone=True), default=timezone.now, server_default=func.now(), nullable=False, ) authors = db.relationship( "Author", secondary=revision_author_table, backref="revisions" ) committer = db.relationship("Author", foreign_keys=[committer_id]) repository = db.relationship("Repository", foreign_keys=[repository_id]) query_class = RepositoryBoundQuery __tablename__ = "revision" # XXX(dcramer): the primary_key doesnt include repo_id at the moment, which is wrong, but # we need to deal w/ that in a followup change __table_args__ = (db.UniqueConstraint("repository_id", "sha", name="unq_revision"),) __repr__ = model_repr("repository_id", "sha", "subject") @property def subject(self): return self.message.splitlines()[0] def generate_diff(self): from zeus.vcs import vcs_client try: return vcs_client.export(self.repository_id, self.sha) except Exception: current_app.logger.exception("generate_diff failure")
class Build(RepositoryBoundMixin, StandardAttributes, db.Model): """ A single build linked to a source. Each Build contains many Jobs. """ source_id = db.Column(GUID, db.ForeignKey("source.id", ondelete="CASCADE"), nullable=False, index=True) number = db.Column(db.Integer, nullable=False) label = db.Column(db.String, nullable=False) status = db.Column(Enum(Status), nullable=False, default=Status.unknown) result = db.Column(Enum(Result), nullable=False, default=Result.unknown) date_created = db.Column( db.TIMESTAMP(timezone=True), nullable=False, default=timezone.now, server_default=func.now(), index=True, ) date_started = db.Column(db.TIMESTAMP(timezone=True), nullable=True) date_finished = db.Column(db.TIMESTAMP(timezone=True), nullable=True) data = db.Column(JSONEncodedDict, nullable=True) provider = db.Column(db.String, nullable=True) external_id = db.Column(db.String(64), nullable=True) url = db.Column(db.String, nullable=True) source = db.relationship("Source", innerjoin=True) stats = db.relationship( "ItemStat", foreign_keys="[ItemStat.item_id]", primaryjoin="ItemStat.item_id == Build.id", viewonly=True, uselist=True, ) __tablename__ = "build" __table_args__ = ( db.UniqueConstraint("repository_id", "number", name="unq_build_number"), db.UniqueConstraint("repository_id", "provider", "external_id", name="unq_build_provider"), ) __repr__ = model_repr("number", "status", "result")
def date_created(cls): return db.Column( db.TIMESTAMP(timezone=True), default=timezone.now, server_default=func.now(), nullable=False, )
class Job(RepositoryBoundMixin, StandardAttributes, db.Model): """ A single job, which is the actual execution unit for a build. """ id = db.Column(GUID, primary_key=True, default=GUID.default_value) build_id = db.Column(GUID, db.ForeignKey('build.id', ondelete='CASCADE'), nullable=False, index=True) number = db.Column(db.Integer, nullable=False) label = db.Column(db.String, nullable=True) status = db.Column(Enum(Status), nullable=False, default=Status.unknown) result = db.Column(Enum(Result), nullable=False, default=Result.unknown) date_started = db.Column(db.TIMESTAMP(timezone=True), nullable=True) date_finished = db.Column(db.TIMESTAMP(timezone=True), nullable=True) data = db.Column(JSONEncodedDict, nullable=True) provider = db.Column(db.String, nullable=True) external_id = db.Column(db.String(64), nullable=True) url = db.Column(db.String, nullable=True) build = db.relationship('Build', backref=db.backref('jobs', order_by='Job.date_created'), innerjoin=True) stats = db.relationship('ItemStat', foreign_keys='[ItemStat.item_id]', primaryjoin='ItemStat.item_id == Job.id', viewonly=True, uselist=True) failures = db.relationship( 'FailureReason', foreign_keys='[FailureReason.job_id]', primaryjoin='FailureReason.job_id == Job.id', viewonly=True, uselist=True, ) __tablename__ = 'job' __table_args__ = (db.UniqueConstraint('build_id', 'number', name='unq_job_number'), db.UniqueConstraint('build_id', 'provider', 'external_id', name='unq_job_provider')) __repr__ = model_repr('build_id', 'number', 'status', 'result')
class ChangeRequest(RepositoryBoundMixin, StandardAttributes, db.Model): number = db.Column(db.Integer, nullable=False) # the parent revision is our base commit that this change request applies to parent_revision_sha = db.Column(db.String(40), nullable=False) # for branch-style change requests (e.g. GitHub Pull Requests) we capture # the 'current revision' in addition to the 'parent revision' head_revision_sha = db.Column(db.String(40), nullable=True) message = db.Column(db.String, nullable=False) author_id = db.Column(GUID, db.ForeignKey("author.id"), index=True, nullable=True) provider = db.Column(db.String, nullable=True) external_id = db.Column(db.String(64), nullable=True) url = db.Column(db.String, nullable=True) data = db.Column(JSONEncodedDict, nullable=True) date_updated = db.Column(db.TIMESTAMP(timezone=True), nullable=True, onupdate=timezone.now) head_revision = db.relationship( "Revision", foreign_keys= "[ChangeRequest.repository_id, ChangeRequest.head_revision_sha]", viewonly=True, ) parent_revision = db.relationship( "Revision", foreign_keys= "[ChangeRequest.repository_id, ChangeRequest.parent_revision_sha]", viewonly=True, ) author = db.relationship("Author") __tablename__ = "change_request" __table_args__ = ( db.ForeignKeyConstraint( ("repository_id", "parent_revision_sha"), ("revision.repository_id", "revision.sha"), ), db.ForeignKeyConstraint( ("repository_id", "head_revision_sha"), ("revision.repository_id", "revision.sha"), ), db.Index("idx_cr_parent_revision", "repository_id", "parent_revision_sha"), db.Index("idx_cr_head_revision", "repository_id", "head_revision_sha"), db.UniqueConstraint("repository_id", "number", name="unq_cr_number"), db.UniqueConstraint("repository_id", "provider", "external_id", name="unq_cr_provider"), ) __repr__ = model_repr("repository_id", "parent_revision_sha") @property def subject(self): return self.message.splitlines()[0]
class Revision(RepositoryBoundMixin, db.Model): sha = db.Column(db.String(40), primary_key=True) author_id = db.Column(GUID, db.ForeignKey("author.id"), index=True, nullable=True) committer_id = db.Column(GUID, db.ForeignKey("author.id"), index=True, nullable=True) message = db.Column(db.Text, nullable=True) parents = db.Column(ARRAY(db.String(40)), nullable=True) branches = db.Column(ARRAY(db.String(128)), nullable=True) date_created = db.Column( db.TIMESTAMP(timezone=True), default=timezone.now, server_default=func.now(), nullable=False, ) date_committed = db.Column( db.TIMESTAMP(timezone=True), default=timezone.now, server_default=func.now(), nullable=False, ) author = db.relationship("Author", foreign_keys=[author_id]) committer = db.relationship("Author", foreign_keys=[committer_id]) __tablename__ = "revision" __table_args__ = (db.UniqueConstraint("repository_id", "sha", name="unq_revision"), ) __repr__ = model_repr("repository_id", "sha", "subject") @property def subject(self): return self.message.splitlines()[0]
class ApiToken(StandardAttributes, db.Model): """ An API token. """ access_token = db.Column( db.String(64), default=lambda: ApiToken.generate_token(), unique=True, nullable=False, ) refresh_token = db.Column( db.String(64), default=lambda: ApiToken.generate_token(), unique=True, nullable=False, ) expires_at = db.Column( db.TIMESTAMP(timezone=True), nullable=True, default=lambda: timezone.now() + DEFAULT_EXPIRATION, ) __tablename__ = "api_token" __repr__ = model_repr("id") @classmethod def generate_token(cls) -> str: return token_hex(32) def is_expired(self) -> bool: if not self.expires_at: return False return timezone.now() >= self.expires_at def refresh(self, expires_at=None): if expires_at is None: expires_at = timezone.now() + DEFAULT_EXPIRATION with db.session.begin_nested(): self.token = type(self).generate_token() self.refresh_token = type(self).generate_token() self.expires_at = expires_at db.session.add(self) db.session.commit()
class User(StandardAttributes, db.Model): """ Actors within Zeus. """ email = db.Column(db.String(128), unique=True, nullable=False) date_active = db.Column( db.TIMESTAMP(timezone=True), nullable=False, default=timezone.now, server_default=func.now(), index=True, ) options = db.relationship( "ItemOption", foreign_keys="[ItemOption.item_id]", primaryjoin="ItemOption.item_id == User.id", viewonly=True, uselist=True, ) __tablename__ = "user" __repr__ = model_repr("email")
class Repository(StandardAttributes, db.Model): """ Represents a single repository. """ # TODO(dcramer): we dont want to be coupled to GitHub (the concept of orgs) # but right now we simply dont care, and this can be refactored later (URLs # are tricky) owner_name = db.Column(db.String(200), nullable=False) name = db.Column(db.String(200), nullable=False) url = db.Column(db.String(200), nullable=False) backend = db.Column( Enum(RepositoryBackend), default=RepositoryBackend.unknown, nullable=False ) status = db.Column( Enum(RepositoryStatus), default=RepositoryStatus.inactive, nullable=False ) provider = db.Column(StrEnum(RepositoryProvider), nullable=False) external_id = db.Column(db.String(64)) data = db.Column(JSONEncodedDict, nullable=True) public = db.Column( db.Boolean, default=False, server_default="false", nullable=False, index=True ) last_update = db.Column(db.TIMESTAMP(timezone=True), nullable=True) last_update_attempt = db.Column(db.TIMESTAMP(timezone=True), nullable=True) options = db.relationship( "ItemOption", foreign_keys="[ItemOption.item_id]", primaryjoin="ItemOption.item_id == Repository.id", viewonly=True, uselist=True, ) query_class = RepositoryAccessBoundQuery __tablename__ = "repository" __table_args__ = ( db.UniqueConstraint("provider", "external_id", name="unq_repo_external_id"), db.UniqueConstraint("provider", "owner_name", "name", name="unq_repo_name"), ) __repr__ = model_repr("name", "url", "provider") def get_vcs(self): from zeus.models import ItemOption from zeus.vcs.git import GitVcs options = dict( db.session.query(ItemOption.name, ItemOption.value).filter( ItemOption.item_id == self.id, ItemOption.name.in_(["auth.username"]) ) ) kwargs = { "path": os.path.join(current_app.config["REPO_ROOT"], self.id.hex), "id": self.id.hex, "url": self.url, "username": options.get("auth.username"), } if self.backend == RepositoryBackend.git: return GitVcs(**kwargs) else: raise NotImplementedError("Invalid backend: {}".format(self.backend)) def get_full_name(self): return "{}/{}/{}".format(self.provider, self.owner_name, self.name)
class Repository(StandardAttributes, db.Model): """ Represents a single repository. """ # TODO(dcramer): we dont want to be coupled to GitHub (the concept of orgs) # but right now we simply dont care, and this can be refactored later (URLs # are tricky) owner_name = db.Column(db.String(200), nullable=False) name = db.Column(db.String(200), nullable=False) url = db.Column(db.String(200), nullable=False) backend = db.Column(Enum(RepositoryBackend), default=RepositoryBackend.unknown, nullable=False) status = db.Column(Enum(RepositoryStatus), default=RepositoryStatus.inactive, nullable=False) provider = db.Column(StrEnum(RepositoryProvider), nullable=False) external_id = db.Column(db.String(64)) data = db.Column(JSONEncodedDict, nullable=True) last_update = db.Column(db.TIMESTAMP(timezone=True), nullable=True) last_update_attempt = db.Column(db.TIMESTAMP(timezone=True), nullable=True) options = db.relationship( 'ItemOption', foreign_keys='[ItemOption.item_id]', primaryjoin='ItemOption.item_id == Repository.id', viewonly=True, uselist=True) query_class = RepositoryAccessBoundQuery __tablename__ = 'repository' __table_args__ = ( db.UniqueConstraint('provider', 'external_id', name='unq_repo_external_id'), db.UniqueConstraint('provider', 'owner_name', 'name', name='unq_repo_name'), ) __repr__ = model_repr('name', 'url', 'provider') def get_vcs(self): from zeus.models import ItemOption from zeus.vcs.git import GitVcs options = dict( db.session.query(ItemOption.name, ItemOption.value).filter( ItemOption.item_id == self.id, ItemOption.name.in_([ 'auth.username', ]))) kwargs = { 'path': os.path.join(current_app.config['REPO_ROOT'], self.id.hex), 'id': self.id.hex, 'url': self.url, 'username': options.get('auth.username'), } if self.backend == RepositoryBackend.git: return GitVcs(**kwargs) else: raise NotImplementedError('Invalid backend: {}'.format( self.backend)) def get_full_name(self): return '{}/{}/{}'.format(self.provider, self.owner_name, self.name)
class Build(RepositoryBoundMixin, StandardAttributes, db.Model): """ A single build. Each Build contains many Jobs. """ ref = db.Column(db.String, nullable=True) revision_sha = db.Column(db.String(40), nullable=True) number = db.Column(db.Integer, nullable=False) label = db.Column(db.String, nullable=True) status = db.Column(Enum(Status), nullable=False, default=Status.unknown) result = db.Column(Enum(Result), nullable=False, default=Result.unknown) date_created = db.Column( db.TIMESTAMP(timezone=True), nullable=False, default=timezone.now, server_default=func.now(), index=True, ) date_started = db.Column(db.TIMESTAMP(timezone=True), nullable=True) date_finished = db.Column(db.TIMESTAMP(timezone=True), nullable=True) data = db.Column(JSONEncodedDict, nullable=True) provider = db.Column(db.String, nullable=True) external_id = db.Column(db.String(64), nullable=True) hook_id = db.Column(GUID, db.ForeignKey("hook.id", ondelete="CASCADE"), nullable=True, index=True) url = db.Column(db.String, nullable=True) author_id = db.Column( GUID, db.ForeignKey("author.id", ondelete="SET NULL"), index=False, nullable=True, ) authors = db.relationship("Author", secondary=build_author_table, backref="builds") revision = db.relationship( "Revision", foreign_keys="[Build.repository_id, Build.revision_sha]", viewonly=True, ) hook = db.relationship("Hook") stats = db.relationship( "ItemStat", foreign_keys="[ItemStat.item_id]", primaryjoin="ItemStat.item_id == Build.id", viewonly=True, uselist=True, ) failures = db.relationship( "FailureReason", foreign_keys="[FailureReason.build_id]", primaryjoin="FailureReason.build_id == Build.id", viewonly=True, uselist=True, ) __tablename__ = "build" __table_args__ = ( db.UniqueConstraint("repository_id", "number", name="unq_build_number"), db.UniqueConstraint("repository_id", "provider", "external_id", name="unq_build_provider"), db.ForeignKeyConstraint( ("repository_id", "revision_sha"), ("revision.repository_id", "revision.sha"), ), db.Index("idx_build_repo_sha", "repository_id", "revision_sha"), db.Index("idx_build_author_date", "author_id", "date_created"), db.Index("idx_build_outcomes", "repository_id", "status", "result", "date_created"), ) __repr__ = model_repr("number", "status", "result")
class Repository(StandardAttributes, db.Model): """ Represents a single repository. """ # TODO(dcramer): we dont want to be coupled to GitHub (the concept of orgs) # but right now we simply dont care, and this can be refactored later (URLs # are tricky) owner_name = db.Column(db.String(200), nullable=False) name = db.Column(db.String(200), nullable=False) url = db.Column(db.String(200), nullable=False) backend = db.Column(Enum(RepositoryBackend), default=RepositoryBackend.unknown, nullable=False) status = db.Column(Enum(RepositoryStatus), default=RepositoryStatus.inactive, nullable=False) provider = db.Column(StrEnum(RepositoryProvider), nullable=False) external_id = db.Column(db.String(64)) data = db.Column(JSONEncodedDict, nullable=True) public = db.Column(db.Boolean, default=False, server_default="false", nullable=False, index=True) last_update = db.Column(db.TIMESTAMP(timezone=True), nullable=True) last_update_attempt = db.Column(db.TIMESTAMP(timezone=True), nullable=True) next_update = db.Column(db.TIMESTAMP(timezone=True), nullable=True) options = db.relationship( "ItemOption", foreign_keys="[ItemOption.item_id]", primaryjoin="ItemOption.item_id == Repository.id", viewonly=True, uselist=True, ) query_class = RepositoryAccessBoundQuery __tablename__ = "repository" __table_args__ = ( db.UniqueConstraint("provider", "external_id", name="unq_repo_external_id"), db.UniqueConstraint("provider", "owner_name", "name", name="unq_repo_name"), ) __repr__ = model_repr("name", "url", "provider") def get_full_name(self): return "{}/{}/{}".format(self.provider, self.owner_name, self.name) @staticmethod def get_lock_key(provider: str, owner_name: str, repo_name: str) -> str: return "repo:{provider}/{owner_name}/{repo_name}".format( provider=provider, owner_name=owner_name, repo_name=repo_name) @classmethod def from_full_name(cls, full_name): provider, owner_name, name = full_name.split("/", 2) return cls.query.filter_by(provider=RepositoryProvider(provider), owner_name=owner_name, name=name).first()