Ejemplo n.º 1
0
class ReviewModel(BaseModel):
    product_id = attributes.UnicodeAttribute(hash_key=True)
    user_id = attributes.UnicodeAttribute()
    review_text = attributes.UnicodeAttribute()

    class Meta(BaseModel.Meta):
        table_name = f"{DYNAMODB_CONFIG.get('env', 'local')}-product-review"
Ejemplo n.º 2
0
 class TestModel(BaseModel):
     Meta = BaseMeta(random.gen_string_id())
     primary_id = attributes.UnicodeAttribute(hash_key=True,
                                              attr_name='pid')
     secondary_id = attributes.UnicodeAttribute(range_key=True,
                                                attr_name='sid')
     data = attributes.UnicodeAttribute(null=True, attr_name='d')
     more_data = attributes.UnicodeAttribute(null=True, attr_name='d2')
Ejemplo n.º 3
0
class AdEntity(EntityBaseMixin, BaseModel):
    """
    Represents a single facebook ad entity
    """

    Meta = EntityBaseMeta(dynamodb_config.AD_ENTITY_TABLE)

    entity_type = Entity.Ad
    campaign_id = attributes.UnicodeAttribute(null=True, attr_name='cid')
    adset_id = attributes.UnicodeAttribute(null=True, attr_name='asid')
Ejemplo n.º 4
0
        class TestModel(BaseModel):

            _fields = {'secondary_id', 'record_type'}

            Meta = BaseMeta(random.gen_string_id())

            primary_id = attributes.UnicodeAttribute(hash_key=True,
                                                     attr_name='pid')
            secondary_id = attributes.UnicodeAttribute(range_key=True,
                                                       attr_name='sid')
            data = attributes.UnicodeAttribute(null=True, attr_name='d')

            record_type = 'SUPER_RECORD'
Ejemplo n.º 5
0
class IdUpdatedAtIndex(indexes.LocalSecondaryIndex):
    """Local secondary index for querying based on id."""
    class Meta:
        """Meta class."""

        projection = indexes.AllProjection()
        index_name = config.get().specs_local_secondary_index_name

        if config.get().stage == config.Stage.TEST:
            host = "http://localhost:8000"

    sub = attributes.UnicodeAttribute(hash_key=True)
    id_updated_at = attributes.UnicodeAttribute(range_key=True)
Ejemplo n.º 6
0
class PlatformToken(BaseModel):
    """
    At this time we have no corporate API for
    (a) management of platform user token assets and linking them to FB entities
    (b) proxying requests to platform over some corporate platform proxy API
        (which would remove the need for passing actual tokens around here)

    When these structural parts are put in,
    remove this table and migrate code to rely on other sources of token data
    """

    Meta = BaseMeta(dynamodb_config.TOKEN_TABLE)

    token_id = attributes.UnicodeAttribute(hash_key=True, attr_name='tid')
    token = attributes.UnicodeAttribute(attr_name='t')
Ejemplo n.º 7
0
class JobReport(BaseModel, MemoizeMixin):
    # this table is split from JobReportEntityExpectation
    # in order to allow blind massive upserts of JobReportEntityExpectation
    # and JobReportEntityOutcome records without caring about
    # present values.
    # Reading these tables combined is very inefficient
    # (O(kn) where k is number of pages in JobReportEntityExpectation
    #  and n number of entities in the system)
    # compared to reading from what unified table could have been,
    # but we care much more about fast idempotent writes in batches
    # more than fast occasional reads.

    Meta = BaseMeta(dynamodb_config.JOB_REPORT_TABLE)

    # value of job_id here could be super weird.
    # It's actually the value of JobReportEntityExpectation.job_id
    # which there never has entity_id, entity_type filled in
    # but here that exact same ID is used as template and
    # entity_id, entity_type are filled in.
    # In this job_id you may see both, entity_type=Campaign and
    # report_variant=Campaign - not something you would see
    # in a wild. This is done just for one reason - create
    # an ID that is compound of JobReportEntityExpectation.job_id
    # and entity_id for use in this table only.
    job_id = attributes.UnicodeAttribute(hash_key=True, attr_name='jid')

    last_progress_dt = attributes.UTCDateTimeAttribute(null=True, attr_name='pdt')
    last_progress_stage_id = attributes.NumberAttribute(null=True, attr_name='pstid')
    last_progress_sweep_id = attributes.UnicodeAttribute(null=True, attr_name='psid')

    last_success_dt = attributes.UTCDateTimeAttribute(null=True, attr_name='sdt')
    last_success_sweep_id = attributes.UnicodeAttribute(null=True, attr_name='ssid')

    last_failure_dt = attributes.UTCDateTimeAttribute(null=True, attr_name='fdt')
    last_failure_stage_id = attributes.NumberAttribute(null=True, attr_name='fstid')
    last_failure_sweep_id = attributes.UnicodeAttribute(null=True, attr_name='fsid')

    last_failure_error = attributes.UnicodeAttribute(null=True, attr_name='fmessage')
    last_failure_bucket = attributes.NumberAttribute(null=True, attr_name='fb')

    last_total_running_time = attributes.NumberAttribute(null=True, attr_name='trt')
    last_total_datapoint_count = attributes.NumberAttribute(null=True, attr_name='tdc')

    last_partial_running_time = attributes.NumberAttribute(null=True, attr_name='prt')
    last_partial_datapoint_count = attributes.NumberAttribute(null=True, attr_name='pdc')

    fails_in_row = attributes.NumberAttribute(attr_name='fir')
Ejemplo n.º 8
0
class AssetScope(BaseModel, MemoizeMixin):
    """
    Stores metadata specific to "Scope" that acts as grouping element
    for a collection of attached top-level assets like Ad Accounts

    This is approximately mapped to a Facebook User (token) or some
    cohesive source of Ad Accounts.

    Initially used for tracking / managing the per-sweep sync of Ad Account IDs from
    Console into our internal store for later iteration over that collection.
    """

    Meta = BaseMeta(dynamodb_config.AD_ACCOUNT_SCOPE_TABLE)

    # scope is an ephemeral scoping element
    # Imagine "operam business manager system user" being one of the scope's values.
    # This is here mostly just to simplify iterating
    # through AAs per given known source.
    # (even if originally we will have only one scope in entire system)
    # At first, scope will be pegged one-to-one to
    # one token to be used for all AAs,
    # (Later this relationship may need to be inverted to
    # allow multiple tokens per AA)
    scope = attributes.UnicodeAttribute(hash_key=True, attr_name='scope')

    # TODO: move storage of scope's API tokens, credentials to this model
    # scope_api_credentials = Blah()
    # while we have only one scope - Console - we can fake it
    # by sending same exact token for all (ahem.. one and only) models
    # This obviously needs to rethought at some point.
    # (Part of the rethinking is that this record has to match a
    #  particular partner API implementation that matches this scope
    #  It's crazy to expect that AUTH will be exactly same and that it will
    #  be just by token.)
    scope_api_token = operam_console_api_config.TOKEN

    platform_token_ids = attributes.UnicodeSetAttribute(default=set,
                                                        attr_name='tids')

    # Memoized in instance to avoid hitting DB all the time we are called.
    @property
    @memoized_property
    def platform_tokens(self):
        """
        Returns a set of actual FB tokens that token_ids attribute point to by ids
        :return:
        """
        return {
            record.token
            for record in PlatformToken.scan(
                PlatformToken.token_id.is_in(*self.platform_token_ids))
        }

    @property
    def platform_token(self):
        try:
            return list(self.platform_tokens)[0]
        except IndexError:
            return None
Ejemplo n.º 9
0
class User(models.Model):
    """ this is the model for our users table """

    username = attributes.UnicodeAttribute(hash_key=True)
    password = attributes.UnicodeAttribute()
    github = attributes.UnicodeAttribute()
    groups = attributes.UnicodeSetAttribute()
    created = attributes.UTCDateTimeAttribute(default=datetime.datetime.now())
    last_login = attributes.UTCDateTimeAttribute(default=datetime.datetime.now())

    class Meta:
        table_name = 'Pdpd_Users'
        region = 'ap-southeast-2'

    @classmethod
    def authenticate(cls, username: str, password: str):
        """ authenticate the user or fail. """
        try:
            user = cls.get(username)
            if user.password != password:
                raise ValueError("Password mismatch.")
            user.update({
                "last_login": {
                    "action": "put",
                    "value": datetime.datetime.now()
                }
            })
            return user
        except cls.DoesNotExist:
            raise ValueError("User does not exist.")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.previous_login = self.last_login

    @property
    def token_payload(self) -> dict:
        """ return the user object details for jwt """
        return {
            'sub': self.username,  # JWT standard uses sub as the subscriber id
            'github': self.github,
            'groups': self.groups,
            'last_login': self.previous_login,
            'this_login': self.last_login
        }
Ejemplo n.º 10
0
class PageVideoEntity(PageEntityBaseMixin, BaseModel):
    """
    Represents a single facebook page video entity
    """

    Meta = EntityBaseMeta(dynamodb_config.PAGE_VIDEO_ENTITY_TABLE)
    entity_id = attributes.UnicodeAttribute(range_key=True, attr_name='eid')

    entity_type = Entity.PageVideo
    _default_bol = True
Ejemplo n.º 11
0
class Thingy(models.Model):
    class Meta:
        table_name = 'Thingy'
        host = None

    id = attrs.NumberAttribute(hash_key=True)
    name = attrs.UnicodeAttribute()

    def __repr__(self):
        return "<Thingy {} name:{}>".format(self.id, self.name)
Ejemplo n.º 12
0
class TeamConf(models.Model):
    """Slackのチームごとの設定を保存するモデル
    """
    team_id = attributes.UnicodeAttribute(hash_key=True)  # Slackのワークスペースのteam_id
    access_token = EncryptedStringAttribute()
    emoji_set = attributes.UnicodeSetAttribute()  # SlackAppが反応するリアクションのemojiのset
    created_at = attributes.UTCDateTimeAttribute(default_for_new=now)

    class Meta:
        region = 'ap-northeast-1'
        table_name = settings.DYNAMODB_TABLE
Ejemplo n.º 13
0
class TestModel(Model):
    class Meta:
        table_name = 'test-table'
        host = os.environ['AWS_DYNAMODB_HOST']
        read_capacity_units = 10
        write_capacity_units = 10

    unicode_attr = attributes.UnicodeAttribute(hash_key=True)
    binary_attr = attributes.BinaryAttribute()
    binary_set_attr = attributes.BinarySetAttribute()
    boolean_attr = attributes.BooleanAttribute()
Ejemplo n.º 14
0
class PageEntityBaseMixin:
    page_id = attributes.UnicodeAttribute(hash_key=True, attr_name='pid')

    bol = attributes.UTCDateTimeAttribute(null=True)
    eol = attributes.UTCDateTimeAttribute(null=True)

    is_accessible = attributes.BooleanAttribute(default=True,
                                                null=True,
                                                attr_name='is_accessible')

    entity_type = None  # will be overridden in subclass
    _additional_fields = {'entity_type'}
Ejemplo n.º 15
0
class PageEntity(ConsoleEntityMixin, BaseModel):
    """
    Represents a single facebook page entity
    """

    Meta = EntityBaseMeta(dynamodb_config.PAGE_ENTITY_TABLE)

    scope = attributes.UnicodeAttribute(hash_key=True, attr_name='scope')
    page_id = attributes.UnicodeAttribute(range_key=True, attr_name='pid')

    # copied indicator of activity from Console DB per each sync
    # (alternative to deletion. To be discussed later if deletion is better)
    is_active = attributes.BooleanAttribute(default=False, attr_name='a')

    is_accessible = attributes.BooleanAttribute(default=True,
                                                attr_name='is_accessible')

    # utilized by logic that prunes out Ad Accounts
    # that are switched to "inactive" on Console
    # Expectation is that after a long-running update job
    # there is a task at the end that goes back and marks
    # all AA records with non-last-sweep_id as "inactive"
    # See https://operam.atlassian.net/browse/PROD-1825 for context
    updated_by_sweep_id = attributes.UnicodeAttribute(null=True, attr_name='u')

    entity_type = Entity.Page

    _additional_fields = {'entity_type'}
    _default_bol = True

    @classmethod
    def upsert_entity_from_console(cls, job_scope: JobScope, entity: Any,
                                   is_accessible: bool):
        cls.upsert(
            job_scope.entity_id,  # scope ID
            entity['ad_account_id'],
            is_active=entity.get('active', True),
            updated_by_sweep_id=job_scope.sweep_id,
            is_accessible=is_accessible,
        )
Ejemplo n.º 16
0
class EntityBaseMixin:
    """
    Use this mixin for describing Facebook entity existence tables
    """

    # Note that each Entity is keyed by, effectively, a compound key: ad_account_id+entity_id
    # This allows us to issue queries like "Get all objects per ad_account_id" rather quickly
    ad_account_id = attributes.UnicodeAttribute(hash_key=True,
                                                attr_name='aaid')

    # Primary Keys

    # Hash Key (old name) == Primary Key (new name)
    # Range Key (old name) == Sort Key (new name) [ == Secondary Key (Cassandra term, used by Daniel D) ]
    # See https://aws.amazon.com/blogs/database/choosing-the-right-dynamodb-partition-key/

    # do NOT set an index on secondary keys (unless you really really need it)
    # In DynamoDB this limits the table size to 10GB
    # Without secondary key index, table size is unbounded.
    # https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html
    # https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html#LSI.ItemCollections.SizeLimit
    entity_id = attributes.UnicodeAttribute(range_key=True, attr_name='eid')

    bol = attributes.UTCDateTimeAttribute(null=True)
    eol = attributes.UTCDateTimeAttribute(null=True)

    is_accessible = attributes.BooleanAttribute(default=True,
                                                null=True,
                                                attr_name='is_accessible')

    # Since we use UpdateItem for inserting records, we must have at least
    # one attribute specified on each model. Normally that would be created_time
    # but FB doesn't have that on all models, so we setup a default
    _default_bol = False

    entity_type = None  # will be overridden in subclass
    _additional_fields = {'entity_type'}
Ejemplo n.º 17
0
class PublicKeyIndex(indexes.GlobalSecondaryIndex):
    """Global secondary index for querying based on the public key."""
    class Meta:
        """Meta class."""

        projection = indexes.AllProjection()
        index_name = config.get().credentials_global_secondary_index_name

        read_capacity_units = 1
        write_capacity_units = 1

        if config.get().stage == config.Stage.TEST:
            host = "http://localhost:8000"

    public_key = attributes.UnicodeAttribute(hash_key=True)
Ejemplo n.º 18
0
class Credentials(models.Model):
    """
    Information about credentials.

    Attrs:
        sub: Unique identifier for a customer
        id: Unique identifier for particular credentials

        public_key: Public identifier for the credentials.
        secret_key_hash: Value derived from the secret key that is safe to store.
        salt: Random value used to generate the credentials.

    """
    class Meta:
        """Meta class."""

        table_name = config.get().credentials_table_name

        if config.get().stage == config.Stage.TEST:
            host = "http://localhost:8000"

    sub = attributes.UnicodeAttribute(hash_key=True)
    id = attributes.UnicodeAttribute(range_key=True)

    public_key = attributes.UnicodeAttribute()
    secret_key_hash = attributes.BinaryAttribute()
    salt = attributes.BinaryAttribute()

    public_key_index = PublicKeyIndex()

    @staticmethod
    def item_to_info(item: "Credentials") -> types.TCredentialsInfo:
        """Convert item to dict with information about the credentials."""
        info: types.TCredentialsInfo = {
            "id": item.id,
            "public_key": item.public_key,
            "salt": item.salt,
        }
        return info

    @classmethod
    def list_(cls, *, sub: types.TSub) -> types.TCredentialsInfoList:
        """
        List all available credentials for a user.

        Filters for a customer and returns all credentials.

        Args:
            sub: Unique identifier for a cutsomer.

        Returns:
            List of information for all credentials of the customer.

        """
        return list(map(cls.item_to_info, cls.query(sub)))

    @classmethod
    def create_update_item(
        cls,
        *,
        sub: types.TSub,
        id_: types.TCredentialsId,
        public_key: types.TCredentialsPublicKey,
        secret_key_hash: types.TCredentialsSecretKeyHash,
        salt: types.TCredentialsSalt,
    ) -> None:
        """
        Create or update a spec.

        Args:
            sub: Unique identifier for a cutsomer.
            id_: Unique identifier for the credentials.
            public_key: Public identifier for the credentials.
            secret_key_hash: Value derived from the secret key that is safe to store.
            salt: Random value used to generate the credentials.

        """
        item = cls(
            sub=sub,
            id=id_,
            public_key=public_key,
            secret_key_hash=secret_key_hash,
            salt=salt,
        )
        item.save()

    @classmethod
    def get_item(
            cls, *, sub: types.TSub, id_: types.TCredentialsId
    ) -> typing.Optional[types.TCredentialsInfo]:
        """
        Retrieve credentials.

        Args:
            sub: Unique identifier for a cutsomer.
            id_: Unique identifier for the credentials.

        Returns:
            Information about the credentials.

        """
        try:
            item = cls.get(hash_key=sub, range_key=id_)
        except cls.DoesNotExist:
            return None

        return cls.item_to_info(item)

    @classmethod
    def get_user(
        cls, *, public_key: types.TCredentialsPublicKey
    ) -> typing.Optional[types.CredentialsAuthInfo]:
        """
        Retrieve a user and information to authenticate the user.

        Args:
            public_key: Public identifier for the credentials.

        Returns:
            Information needed to authenticate the user.

        """
        item = next(cls.public_key_index.query(hash_key=public_key), None)

        if item is None:
            return None

        return types.CredentialsAuthInfo(sub=item.sub,
                                         secret_key_hash=item.secret_key_hash,
                                         salt=item.salt)

    @classmethod
    def delete_item(cls, *, sub: types.TSub,
                    id_: types.TCredentialsId) -> None:
        """
        Delete the credentials.

        Args:
            sub: Unique identifier for a cutsomer.
            id_: Unique identifier for the credentials.

        """
        try:
            item = cls.get(hash_key=sub, range_key=id_)
            item.delete()
        except cls.DoesNotExist:
            pass

    @classmethod
    def delete_all(cls, *, sub: types.TSub) -> None:
        """
        Delete all the credentials for a user.

        Args:
            sub: Unique identifier for a cutsomer.

        """
        items = cls.query(hash_key=sub)
        with cls.batch_write() as batch:
            for item in items:
                batch.delete(item)
Ejemplo n.º 19
0
class TodoDAO(models.Model):
    class Meta:
        table_name = "dev-todo-k8s-todos"
        host = "http://localstack:4569"

    assignee = attributes.NumberAttribute(hash_key=True)
    id = attributes.NumberAttribute(range_key=True)
    status = attributes.NumberAttribute()
    title = attributes.UnicodeAttribute()
    description = attributes.UnicodeAttribute(null=True)
    created_on = attributes.NumberAttribute()
    due_on = attributes.NumberAttribute()
    completed_on = attributes.NumberAttribute(null=True)

    last_updated = attributes.NumberAttribute()
    assingee_status_index = AssigneeStatusIndex()

    @classmethod
    def from_proto(cls, todo_proto):
        """
        Get a ``TodoDAO`` object from a proto message.

        Args:
            todo_proto (todo_pb2.Todo): The proto message.

        Returns:
            TodoDAO
        """
        todo = TodoDAO(hash_key=todo_proto.assignee,
                       range_key=todo_proto.id,
                       status=todo_proto.status,
                       title=todo_proto.title,
                       description=todo_proto.description,
                       created_on=todo_proto.createdOn,
                       due_on=todo_proto.dueOn,
                       completed_on=todo_proto.completedOn)
        return todo

    def save(self, conditional_operator=None, **expected_values):
        if self.status is None:
            self.status = Status.TODO

        timestamp = get_timestamp()
        self.last_updated = timestamp
        if not self.created_on:
            self.created_on = timestamp
        if not self.id:
            self.id = get_todo_id(self)

        return super(TodoDAO, self).save(conditional_operator,
                                         **expected_values)

    def to_proto(self):
        return Todo(id=self.id,
                    assignee=self.assignee,
                    status=self.status,
                    title=self.title,
                    description=self.description,
                    createdOn=self.created_on,
                    dueOn=self.due_on,
                    completedOn=self.completed_on)
Ejemplo n.º 20
0
class Spec(models.Model):
    """
    Information about a spec.

    Attrs:
        UPDATED_AT_LATEST: Constant for what to set updated_at to to indicate it is the
            latest record

        sub: Unique identifier for a customer
        id: Unique identifier for a spec for a package derrived from the name
        name: The display name of the spec
        updated_at: The last time the spec version was updated in integer seconds since
            epoch stored as a string or 'latest' for the copy of the latest version of
            the spec.

        version: The version of a spec for a package
        title: The title of a spec
        description: The description of a spec
        model_count: The number of 'x-tablename' and 'x-inherits' in a spec

        updated_at_id: Combination of 'updated_at' and 'id' separeted with #
        id_updated_at: Combination of 'id' and 'updated_at' separeted with #

        id_updated_at_index: Index for querying id_updated_at efficiently

    """

    UPDATED_AT_LATEST = "latest"

    class Meta:
        """Meta class."""

        table_name = config.get().specs_table_name

        if config.get().stage == config.Stage.TEST:
            host = "http://localhost:8000"

    sub = attributes.UnicodeAttribute(hash_key=True)
    id = attributes.UnicodeAttribute()
    name = attributes.UnicodeAttribute()
    updated_at = attributes.UnicodeAttribute()

    version = attributes.UnicodeAttribute()
    title = attributes.UnicodeAttribute(null=True)
    description = attributes.UnicodeAttribute(null=True)
    model_count = attributes.NumberAttribute()

    updated_at_id = attributes.UnicodeAttribute(range_key=True)
    id_updated_at = attributes.UnicodeAttribute()

    id_updated_at_index = IdUpdatedAtIndex()

    @classmethod
    def count_customer_models(cls, *, sub: types.TSub) -> int:
        """
        Count the number of models on the latest specs for a customer.

        Filters for a particular customer and updated_at_id to start with
        'latest#' and sums over model_count.

        Args:
            sub: Unique identifier for the customer.

        Returns:
            The sum of the model count on the latest version of each unique spec for
            the customer.

        """
        return sum(
            map(
                lambda item: int(item.model_count),
                cls.query(
                    sub,
                    cls.updated_at_id.startswith(f"{cls.UPDATED_AT_LATEST}#"),
                ),
            ))

    @staticmethod
    def calc_id(name: types.TSpecName) -> types.TSpecId:
        """Calculate the id based on the name."""
        return utils.canonicalize_name(name)

    @classmethod
    def calc_index_values(cls, *, updated_at: types.TSpecUpdatedAt,
                          id_: types.TSpecId) -> TSpecIndexValues:
        """
        Calculate the updated_at_id value.

        Args:
            updated_at: The value for updated_at
            id_: The value for id

        Returns:
            The value for updated_at_id

        """
        # Zero pad updated_at if it is not latest
        if updated_at != cls.UPDATED_AT_LATEST:
            updated_at = updated_at.zfill(20)

        return TSpecIndexValues(
            updated_at_id=f"{updated_at}#{id_}",
            id_updated_at=f"{id_}#{updated_at}",
        )

    @classmethod
    def create_update_item(
        cls,
        *,
        sub: types.TSub,
        name: types.TSpecName,
        version: types.TSpecVersion,
        model_count: types.TSpecModelCount,
        title: types.TSpecTitle = None,
        description: types.TSpecDescription = None,
    ) -> None:
        """
        Create or update an item.

        Creates or updates 2 items in the database. The updated_at attribute for the
        first is calculated based on seconds since epoch and for the second is set to
        'latest'. Also computes the sort key updated_at_id based on updated_at and
        id.

        Args:
            sub: Unique identifier for a cutsomer.
            name: The display name of the spec.
            version: The version of the spec.
            model_count: The number of models in the spec.
            title: The title of a spec
            description: The description of a spec

        """
        id_ = cls.calc_id(name)

        # Write item
        updated_at = str(int(time.time()))
        index_values = cls.calc_index_values(updated_at=updated_at, id_=id_)
        item = cls(
            sub=sub,
            id=id_,
            name=name,
            updated_at=updated_at,
            version=version,
            title=title,
            description=description,
            model_count=model_count,
            updated_at_id=index_values.updated_at_id,
            id_updated_at=index_values.id_updated_at,
        )
        item.save()

        # Write latest item
        updated_at_latest = cls.UPDATED_AT_LATEST
        index_values_latest = cls.calc_index_values(
            updated_at=updated_at_latest, id_=id_)
        item_latest = cls(
            sub=sub,
            id=id_,
            name=name,
            version=version,
            updated_at=updated_at,
            title=title,
            description=description,
            model_count=model_count,
            updated_at_id=index_values_latest.updated_at_id,
            id_updated_at=index_values_latest.id_updated_at,
        )
        item_latest.save()

    @classmethod
    def get_latest_version(cls, *, sub: types.TSub,
                           name: types.TSpecId) -> types.TSpecVersion:
        """
        Get the latest version for a spec.

        Raises NotFoundError if the spec is not found in the database.

        Calculates updated_at_id by setting updated_at to latest and using the
        id. Tries to retrieve an item for a customer based on the sort key.

        Args:
            sub: Unique identifier for a cutsomer.
            name: The display name of the spec.

        Returns:
            The latest version of the spec.

        """
        id_ = cls.calc_id(name)
        try:
            item = cls.get(
                hash_key=sub,
                range_key=cls.calc_index_values(
                    updated_at=cls.UPDATED_AT_LATEST, id_=id_).updated_at_id,
            )
            return item.version
        except cls.DoesNotExist as exc:
            raise exceptions.NotFoundError(
                f"the spec {name=}, {id_=} does not exist for customer {sub=}"
            ) from exc

    @staticmethod
    def item_to_info(item: "Spec") -> types.TSpecInfo:
        """Convert item to dict with information about the spec."""
        info: types.TSpecInfo = {
            "name": item.name,
            "id": item.id,
            "updated_at": int(item.updated_at),
            "version": item.version,
            "model_count": int(item.model_count),
        }
        if item.title is not None:
            info["title"] = item.title
        if item.description is not None:
            info["description"] = item.description
        return info

    @classmethod
    def list_(cls, *, sub: types.TSub) -> types.TSpecInfoList:
        """
        List all available specs for a customer.

        Filters for a customer and for updated_at_id to start with latest.

        Args:
            sub: Unique identifier for a cutsomer.

        Returns:
            List of information for all specs for the customer.

        """
        return list(
            map(
                cls.item_to_info,
                cls.query(
                    sub,
                    cls.updated_at_id.startswith(f"{cls.UPDATED_AT_LATEST}#"),
                ),
            ))

    @classmethod
    def get_item(cls, *, sub: types.TSub,
                 name: types.TSpecName) -> types.TSpecInfo:
        """
        Retrieve a spec from the database.

        Raises NotFoundError if the spec was not found.

        Args:
            sub: Unique identifier for a cutsomer.
            name: The display name of the spec.

        Returns:
            Information about the spec

        """
        id_ = cls.calc_id(name)
        updated_at_id = cls.calc_index_values(updated_at=cls.UPDATED_AT_LATEST,
                                              id_=id_).updated_at_id
        try:
            item = cls.get(hash_key=sub, range_key=updated_at_id)
            return cls.item_to_info(item)
        except cls.DoesNotExist as exc:
            raise exceptions.NotFoundError(
                f"could not find spec {name=} for user {sub=} in the database"
            ) from exc

    @classmethod
    def delete_item(cls, *, sub: types.TSub, name: types.TSpecName) -> None:
        """
        Delete a spec from the database.

        Args:
            sub: Unique identifier for a cutsomer.
            name: The display name of the spec.

        """
        id_ = cls.calc_id(name)
        items = cls.id_updated_at_index.query(
            sub, cls.id_updated_at.startswith(f"{id_}#"))
        with cls.batch_write() as batch:
            for item in items:
                batch.delete(item)

    @classmethod
    def list_versions(cls, *, sub: types.TSub,
                      name: types.TSpecName) -> types.TSpecInfoList:
        """
        List all available versions for a spec for a customer.

        Filters for a customer and for updated_at_id to start with latest.

        Args:
            sub: Unique identifier for a cutsomer.
            name: The display name of the spec.

        Returns:
            List of information for all versions of a spec for the customer.

        """
        id_ = cls.calc_id(name)
        items = cls.id_updated_at_index.query(
            sub,
            cls.id_updated_at.startswith(f"{id_}#"),
        )
        items_no_latest = filter(
            lambda item: not item.updated_at_id.startswith(
                f"{cls.UPDATED_AT_LATEST}#"),
            items,
        )
        return list(map(cls.item_to_info, items_no_latest))

    @classmethod
    def delete_all(cls, *, sub: types.TSub) -> None:
        """
        Delete all the specs for a user.

        Args:
            sub: Unique identifier for a cutsomer.

        """
        items = cls.query(hash_key=sub)
        with cls.batch_write() as batch:
            for item in items:
                batch.delete(item)
Ejemplo n.º 21
0
class AdAccountEntity(ConsoleEntityMixin, BaseModel):
    """
    Represents a single facebook ad account entity
    """

    Meta = BaseMeta(dynamodb_config.AD_ACCOUNT_ENTITY_TABLE)

    _additional_fields = {'entity_type'}

    # scope is an ephemeral scoping element
    # Imagine "operam business manager system user" being one of the scope's values.
    # This is here mostly just to simplify iterating
    # through AAs per given known source.
    # (even if originally we will have only one scope in entire system)
    # At first, scope will be pegged one-to-one to
    # one token to be used for all AAs,
    # (Later this relationship may need to be inverted to
    # allow multiple tokens per AA)
    scope = attributes.UnicodeAttribute(hash_key=True, attr_name='scope')

    ad_account_id = attributes.UnicodeAttribute(range_key=True,
                                                attr_name='aaid')

    # copied indicator of activity from Console DB per each sync
    # (alternative to deletion. To be discussed later if deletion is better)
    is_active = attributes.BooleanAttribute(default=False, attr_name='a')

    is_accessible = attributes.BooleanAttribute(default=True,
                                                attr_name='is_accessible')

    # Provides an option to manually disable accounts from syncing, even if they are imported as active from console.
    manually_disabled = attributes.BooleanAttribute(default=False,
                                                    attr_name='man_dis')

    # utilized by logic that prunes out Ad Accounts
    # that are switched to "inactive" on Console
    # Expectation is that after a long-running update job
    # there is a task at the end that goes back and marks
    # all AA records with non-last-sweep_id as "inactive"
    # See https://operam.atlassian.net/browse/PROD-1825 for context
    updated_by_sweep_id = attributes.UnicodeAttribute(null=True, attr_name='u')

    # Each AdAccount on FB side can be set to a particular timezone
    # A lot of reporting on FB is pegged to a "day" that is interpreted
    # in that AdAccount's timezone (not UTC).
    timezone = attributes.UnicodeAttribute(attr_name='tz')

    # manually controlled through Dynamo UI. Here we just read it
    # does not have to be set for majority of records.
    score_multiplier = attributes.NumberAttribute(null=True,
                                                  attr_name='score_skew')
    refresh_if_older_than = attributes.UTCDateTimeAttribute(
        null=True, attr_name='refresh_to')

    entity_type = Entity.AdAccount

    @property
    @memoized_property
    def scope_model(self):
        from common.store.scope import AssetScope

        return AssetScope.get(self.scope)

    def to_fb_sdk_ad_account(self, api=None):
        """
        Returns an instance of Facebook Ads SDK AdAccount model
        with ID matching this DB model's ID

        :param api: FB Ads SDK Api instance with token baked in.
        """
        from facebook_business.api import FacebookAdsApi, FacebookSession
        from facebook_business.adobjects.adaccount import AdAccount

        if not api:
            # This is very expensive call. Takes 2 DB hits to get token value
            # Try to pass api value at all times in prod code to avoid using this.
            # Allowing to omit the API value to simplify use of this API in
            # testing in console.
            api = FacebookAdsApi(
                FacebookSession(access_token=self.scope_model.platform_token))

        return AdAccount(fbid=f'act_{self.ad_account_id}', api=api)

    @classmethod
    def upsert_entity_from_console(cls, job_scope: JobScope, entity: Any,
                                   is_accessible: bool):
        cls.upsert(
            job_scope.entity_id,  # scope ID
            entity['ad_account_id'],
            is_active=entity.get('active', True),
            updated_by_sweep_id=job_scope.sweep_id,
            is_accessible=is_accessible,
        )