class UserAccount(db.Model): # type: ignore __bind_key__ = 'dashboard_service' __tablename__ = 'user_account' id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) joined_dt = db.Column(db.DateTime(), server_default=func.now()) closed_dt = db.Column(db.DateTime()) inactive_dt = db.Column(db.DateTime()) is_closed = db.Column(db.Boolean, default=False) is_active = db.Column(db.Boolean, default=True) info = db.relationship('UserInfo', backref='account', uselist=False, cascade="all, delete-orphan") privacy = db.relationship('UserPrivacy', backref='account', uselist=False, cascade="all, delete-orphan") workspaces = db.relationship('Workspace', secondary="workspaces_members", lazy='subquery', backref=db.backref('accounts', lazy=True)) orgs = db.relationship('Org', secondary="orgs_members", lazy='subquery', backref=db.backref('accounts', lazy=True)) # direct access to the unique personal org # but this org is also part of the many to many relationship personal_org = db.relationship('Org', backref='account', uselist=False, cascade="all, delete-orphan") def to_public_dict(self): return { "id": shortuuid.encode(self.id), "joined": "{}Z".format(self.joined_dt.isoformat()), "workspaces": [w.to_dict() for w in self.workspaces], "orgs": [o.to_dict() for o in self.orgs] } def to_short_dict(self): return { "id": shortuuid.encode(self.id), "joined": "{}Z".format(self.joined_dt.isoformat()), "org": { "name": self.personal_org.name }, "profile": self.info.to_public_dict() }
class AccessToken(db.Model, OAuth2TokenMixin): # type: ignore __bind_key__ = 'auth_service' id = db.Column(UUIDType(binary=False), primary_key=True, unique=True, default=uuid.uuid4) name = db.Column(db.String(), nullable=False) account_id = db.Column(UUIDType(binary=False), db.ForeignKey('account.id', ondelete='CASCADE'), nullable=False) last_used_on = db.Column(db.DateTime()) def to_dict(self): last_used = None if self.last_used_on: last_used = "{}Z".format(self.last_used_on.isoformat()) return { "id": shortuuid.encode(self.id), "name": self.name, "account_id": shortuuid.encode(self.account_id), "access_token": self.access_token, "refresh_token": self.refresh_token, "client_id": self.client_id, "scope": self.scope, "token_type": self.token_type, "issued_at": self.issued_at, "expires_in": self.expires_in, "last_used": last_used, "revoked": self.revoked }
class Schedule(db.Model): # type: ignore __bind_key__ = 'experiment_service' id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) account_id = db.Column(UUIDType(binary=False), nullable=False, index=True) scheduled = db.Column(db.DateTime(), nullable=False) status = db.Column(db.Enum(ScheduleStatus), nullable=False, default=ScheduleStatus.pending) org_id = db.Column(UUIDType(binary=False), nullable=False, index=True) workspace_id = db.Column(UUIDType(binary=False), nullable=False, index=True) experiment_id = db.Column(UUIDType(binary=False), nullable=False, index=True) token_id = db.Column(UUIDType(binary=False), nullable=False) definition = db.Column(JSONB()) info = db.Column(JSONB()) def to_dict(self): return { "id": shortuuid.encode(self.id), "account_id": shortuuid.encode(self.account_id), "org_id": shortuuid.encode(self.org_id), "workspace_id": shortuuid.encode(self.workspace_id), "experiment_id": shortuuid.encode(self.experiment_id), "token_id": shortuuid.encode(self.token_id), "scheduled": "{}Z".format(self.scheduled.isoformat()), "definition": self.definition, "info": self.info }
class Recommendation(db.Model): # type: ignore __bind_key__ = 'experiment_service' id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) recommendation_type = db.Column(db.String(), index=True) tags = db.relationship('RecommendationTag', secondary="recommendation_tags_assoc", backref=db.backref('recommendations', lazy=True)) checksum = db.Column(db.String(), nullable=False) last_updated = db.Column(db.DateTime()) rating = db.Column(db.Float, default=3.0) meta = db.Column(JSONB()) data = db.Column(JSONB(), nullable=False) def to_dict(self): data = self.data if self.recommendation_type == "experiment": data = data["hashed"] return { "id": str(self.id), "type": self.recommendation_type, "tags": [tag.value for tag in self.tags], "checksum": self.checksum, "last_updated": "{}Z".format(self.last_updated.isoformat()), "meta": self.meta, "data": data }
class UserPrivacy(db.Model): # type: ignore __bind_key__ = 'dashboard_service' id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) account_id = db.Column(UUIDType(binary=False), db.ForeignKey('user_account.id'), nullable=False) last_changed = db.Column(db.DateTime(), server_default=func.now()) details = db.Column(JSONB())
class Discovery(db.Model): # type: ignore __bind_key__ = 'experiment_service' id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) account_id = db.Column(UUIDType(binary=False), nullable=False, index=True) received_date = db.Column(db.DateTime(), server_default=func.now()) workspace_id = db.Column(UUIDType(binary=False), nullable=False, index=True) payload = db.Column(JSONB())
class Account(db.Model): # type: ignore __bind_key__ = 'auth_service' __table_args__ = (db.UniqueConstraint('oauth_provider', 'oauth_provider_sub', name='oauth_provider_sub_uniq'), ) id = db.Column(UUIDType(binary=False), primary_key=True, unique=True, default=uuid.uuid4) joined_on = db.Column(db.DateTime(timezone=True), server_default=func.now(), nullable=False) closed_since = db.Column(db.DateTime(timezone=True), nullable=True) inactive_since = db.Column(db.DateTime(timezone=True), nullable=True) is_closed = db.Column(db.Boolean, default=False) is_active = db.Column(db.Boolean, default=True) oauth_provider = db.Column(db.String, nullable=True) oauth_provider_sub = db.Column(db.String, nullable=True) access_tokens = db.relationship('AccessToken', backref='account', cascade="all, delete-orphan") client = db.relationship('Client', backref='account', uselist=False, cascade="all, delete-orphan") local = db.relationship('LocalAccount', backref='account', uselist=False, cascade="all, delete-orphan") def to_dict(self): inactive = closed = None if self.inactive_since: inactive = "{}Z".format(self.inactive_since.isoformat()) if self.closed_since: closed = "{}Z".format(self.closed_since.isoformat()) return { "id": str(self.id), "short_id": shortuuid.encode(self.id), "closed": self.is_closed, "active": self.is_active, "joined_on": "{}Z".format(self.joined_on.isoformat()), "inactive_since": inactive, "closed_since": closed, "client": self.client.to_dict() if self.client else None, "tokens": [t.to_dict() for t in self.access_tokens] } def turn_inactive(self): self.is_active = False self.inactive_since = datetime.utcnow() def turn_active(self): self.is_active = True self.inactive_since = None def close_account(self): self.is_closed = False self.closed_since = datetime.utcnow()
class APIAccessToken(db.Model): # type: ignore __bind_key__ = 'api_service' __tablename__ = 'api_access_token' __table_args__ = (db.UniqueConstraint('name', 'account_id', name='name_account_uniq'), ) id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) name = db.Column(db.String, nullable=False) account_id = db.Column(UUIDType(binary=False), nullable=False) last_used_on = db.Column(db.DateTime()) client_id = db.Column(db.String(48)) token_type = db.Column(db.String(40)) access_token = db.Column(db.String(255), unique=True, nullable=False) refresh_token = db.Column(db.String(255), index=True) scope = db.Column(db.Text, default='') # be conservative revoked = db.Column(db.Boolean, nullable=False, default=True) issued_at = db.Column(db.Integer, nullable=False) expires_in = db.Column(db.Integer, nullable=False, default=0) def get_scope(self): return self.scope def get_expires_in(self): return self.expires_in def get_expires_at(self): return self.issued_at + self.expires_in def is_expired(self): now = datetime.utcnow().timestamp() return self.get_expires_at() < now def is_active(self): now = datetime.utcnow().timestamp() return self.get_expires_at() >= now def revoke(self): self.revoked = True def to_dict(self): last_used = None if self.last_used_on: last_used = self.last_used_on.replace( tzinfo=timezone.utc).timestamp() return { "id": str(self.id), "account_id": str(self.account_id), "access_token": self.access_token, "refresh_token": self.refresh_token, "client_id": self.client_id, "scope": self.scope, "token_type": self.token_type, "issued_at": self.issued_at, "expires_in": self.expires_in, "last_used": last_used, "revoked": self.revoked } @staticmethod def from_dict(token: Dict[str, Any]) -> 'APIAccessToken': """ Create or update a token from the source access token. On update, only the scope, revoked and dates properties are changed. Others are left as they are. """ access_token = APIAccessToken.get_by_token(token["access_token"]) if not access_token: access_token = APIAccessToken() access_token.id = shortuuid.decode(token["id"]) access_token.account_id = shortuuid.decode(token["account_id"]) access_token.access_token = token["access_token"] access_token.client_id = token["client_id"] access_token.name = token["name"] access_token.refresh_token = token["refresh_token"] access_token.scope = token["scope"] access_token.revoked = token["revoked"] access_token.issued_at = token["issued_at"] access_token.expires_in = token["expires_in"] return access_token @staticmethod def get_by_token(access_token: str) -> Optional['APIAccessToken']: return APIAccessToken.query.filter( APIAccessToken.access_token == access_token).first() @staticmethod def get_by_id_for_account(account_id: str, token_id: str) -> Optional['APIAccessToken']: return APIAccessToken.query.filter( APIAccessToken.account_id == account_id, APIAccessToken.id == token_id).first() @staticmethod def get_all_for_account(account_id: str) -> List['APIAccessToken']: return APIAccessToken.query.filter( APIAccessToken.account_id == account_id).all() @staticmethod def get_active_for_account(account_id: str) -> List['APIAccessToken']: non_revoked_tokens = APIAccessToken.query.filter( APIAccessToken.revoked == False, APIAccessToken.account_id == account_id).all() return [token for token in non_revoked_tokens if token.is_active()]
class Experiment(db.Model): # type: ignore __bind_key__ = 'experiment_service' id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) shared_ref = db.Column(UUIDType(binary=False), nullable=False, index=True) account_id = db.Column(UUIDType(binary=False), nullable=False, index=True) created_date = db.Column(db.DateTime(), server_default=func.now()) updated_date = db.Column(db.DateTime(), server_default=func.now(), server_onupdate=func.now()) suggested_experiment_id = db.Column(UUIDType(binary=False), index=True) org_id = db.Column(UUIDType(binary=False), nullable=False, index=True) workspace_id = db.Column(UUIDType(binary=False), nullable=False, index=True) executions = db.relationship('Execution', backref='experiment', cascade="all, delete-orphan") payload = db.Column(NestedMutable.as_mutable(JSONB), nullable=False) def to_dict(self, with_payload: bool = True): updated_date = None timestamp = self.created_date.timestamp() if self.updated_date: updated_date = "{}Z".format(self.updated_date.isoformat()) timestamp = self.updated_date.timestamp() d = { "id": shortuuid.encode(self.id), "ref": shortuuid.encode(self.shared_ref), "created_date": "{}Z".format(self.created_date.isoformat()), "updated_date": updated_date, "timestamp": timestamp, "org": shortuuid.encode(self.org_id), "workspace": shortuuid.encode(self.workspace_id), "title": self.payload.get("title"), "description": self.payload.get("description") } if with_payload: d["payload"] = self.payload return d def to_public_dict(self, with_payload: bool = True): updated_date = None timestamp = self.created_date.timestamp() if self.updated_date: updated_date = "{}Z".format(self.updated_date.isoformat()) timestamp = self.updated_date.timestamp() d = { "id": shortuuid.encode(self.id), "ref": shortuuid.encode(self.shared_ref), "created_date": "{}Z".format(self.created_date.isoformat()), "updated_date": updated_date, "timestamp": timestamp, "org": shortuuid.encode(self.org_id), "workspace": shortuuid.encode(self.workspace_id), "title": self.payload.get("title"), "description": self.payload.get("description"), "tags": [tag for tag in self.payload.get("tags", [])] } if with_payload: d["payload"] = self.payload return d @staticmethod def get_by_id(exp_id: Union[str, uuid.UUID]) -> Optional['Experiment']: if not exp_id: return None if isinstance(exp_id, str): try: exp_id = shortuuid.decode(exp_id) except ValueError: return None return Experiment.query.filter(Experiment.id == exp_id).first() def get_execution(self, timestamp: int) -> Optional['Execution']: return Execution.query.filter( Execution.experiment_id == self.id, Execution.timestamp == timestamp).first()
class Org(db.Model): # type: ignore __bind_key__ = 'dashboard_service' id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) # only set when this is a personal org linked to a single account, # otherwise it's not set account_id = db.Column(UUIDType(binary=False), db.ForeignKey('user_account.id'), nullable=True) name = db.Column(db.String(), nullable=False, unique=True) name_lower = db.Column(db.String(), nullable=False, unique=True) kind = db.Column(db.Enum(OrgType), nullable=False, default=OrgType.personal) created_on = db.Column(db.DateTime(), server_default=func.now()) workspaces = db.relationship('Workspace', backref='org', cascade="all, delete-orphan") settings = db.Column(JSONB(), nullable=False, default=DEFAULT_ORG_SETTINGS) def to_dict(self, public_workspaces_only: bool = False): workspaces = [] for w in self.workspaces: if public_workspaces_only and w.kind != WorkspaceType.public: continue workspaces.append({"id": shortuuid.encode(w.id), "name": w.name}) return { "id": shortuuid.encode(self.id), "name": self.name, "settings": self.settings, "type": self.kind.value, "created_on": "{}Z".format(self.created_on.isoformat()), "workspaces": workspaces } def to_short_dict(self): return { "id": shortuuid.encode(self.id), "name": self.name, "created_on": "{}Z".format(self.created_on.isoformat()), "settings": self.settings, "type": self.kind.value } @staticmethod def get_next_available_name(suggested_name: str) -> str: """ Return the next available name prefixed by the given suggested name and suffixed by a number between 0 and 1000. If `suggested_name` is not used yet, return it as the available name """ while True: has_org = Org.query.filter(Org.name == suggested_name).first() if not has_org: return suggested_name suggested_name = "{}{}".format(suggested_name, secrets.randbelow(1000)) @staticmethod def get_by_id(org_id: Union[str, uuid.UUID]) -> 'Org': """ Lookup an organization by its identifier """ return Org.query.filter(Org.id == org_id).first() @staticmethod def find_by_name(org_name: str) -> 'Org': """ Lookup an organization by its name """ return Org.query.filter(Org.name_lower == org_name.lower()).first() def find_workspace_by_name(self, workspace_name: str) -> Optional[Workspace]: """ Lookup a workspace in the organization by its name """ w_name = workspace_name.lower() for workspace in self.workspaces: if workspace.name_lower == w_name: return workspace return None def is_member(self, account_id: Union[str, uuid.UUID]) -> bool: """ Return `True` when the given account is a member of the organization """ return OrgsMembers.query.filter( OrgsMembers.org_id == self.id, OrgsMembers.account_id == account_id).first() is not None def is_owner(self, account_id: Union[str, uuid.UUID]) -> bool: """ Return `True` when the given account is an owner of the organization """ return OrgsMembers.query.filter( OrgsMembers.org_id == self.id, OrgsMembers.is_owner == True, OrgsMembers.account_id == account_id).first() is not None def has_single_owner(self) -> bool: """ Return `True` if only one owner exists for this organization """ return OrgsMembers.query.filter( OrgsMembers.org_id == self.id, OrgsMembers.is_owner == True).count() == 1 def make_member(self, account_id: Union[str, uuid.UUID]): """ Turn an user as a member only of this organization. The user must already be part of this organization, this is mostly therefore useful when moving an owner down to simple member. """ membership = OrgsMembers.query.filter( OrgsMembers.org_id == self.id, OrgsMembers.account_id == account_id).first() if membership: membership.is_owner = False def make_owner(self, account_id: Union[str, uuid.UUID]): """ Turn an user as an owner of this organization. """ membership = OrgsMembers.query.filter( OrgsMembers.org_id == self.id, OrgsMembers.account_id == account_id).first() if membership: membership.is_owner = True def add_member(self, account_id: Union[str, uuid.UUID]) -> OrgsMembers: """ Add this user to the organization as a member """ membership = OrgsMembers(org_id=self.id, account_id=account_id) db.session.add(membership) return membership def remove_member(self, account_id: Union[str, uuid.UUID]): """ Remove this member from the organization """ OrgsMembers.query.filter( OrgsMembers.org_id == self.id, OrgsMembers.account_id == account_id).delete() def get_public_workspace_ids(self) -> List[uuid.UUID]: """ List all public workspaces in this organization and return their identifiers """ result = db.session.query(Workspace.id).filter( Workspace.org_id == self.id, Workspace.kind == WorkspaceType.public).all() if not result: return [] return result[0]
class UserInfo(db.Model): # type: ignore __bind_key__ = 'dashboard_service' id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) account_id = db.Column(UUIDType(binary=False), db.ForeignKey('user_account.id'), nullable=False) last_updated = db.Column(db.DateTime(), server_default=func.now(), onupdate=func.current_timestamp()) verified_email = db.Column(db.Boolean(), default=False) # values for search purpose mostly username = db.Column(db.String, index=True, nullable=True) fullname = db.Column(db.String, index=True, nullable=True) details = db.Column(EncryptedType(db.String, get_user_info_secret_key, AesEngine, 'pkcs5'), nullable=False) @staticmethod def get_for_account(account_id: Union[str, uuid.UUID]) -> 'UserInfo': """ Lookup user info for the given user account """ return UserInfo.query.filter(UserInfo.account_id == account_id).first() @property def profile(self) -> Dict[str, Any]: """ The user's profile """ return json.loads(self.details) @profile.setter def profile(self, p: Dict[str, Any]): """ Set the user's profile from the given payload The payload is serialized to JSON and stored in the `details` property """ self.details = json.dumps(p) def to_dict(self): return { "id": shortuuid.encode(self.id), "account": shortuuid.encode(self.account.id), "profile": self.profile } def to_public_dict(self): p = self.profile return { "id": shortuuid.encode(self.id), "username": p.get("preferred_username"), "name": p.get("name"), "picture": p.get("picture") }