Beispiel #1
0
    def load_stream(self, session, aggregate_uuid):
        """Query an event stream from an aggregate.

        Args:
            session (Session): A session context into which operate.
            aggregate_uuid (str): An UUID4 string. The aggregate from
                which query the events.

        Returns:
            Query: A Query with rows from the "events" table.

        """
        try:
            aggregate = session.query(AggregateModel).options(
                joinedload('events')).filter(
                    AggregateModel.uuid == str(aggregate_uuid)).one()
        except NoResultFound:
            raise NotFoundError(f'Aggregate not found: {aggregate_uuid}')

        version = aggregate.version
        events = aggregate.events
        event_stream = EventStream(version, events)

        logger.debug(event_stream)

        return event_stream
Beispiel #2
0
def translations():
    """Alternative route to get only tranlation JSON.

    This route is used by the frontend to poll translations from a
    single page and dynamically update the translations table.

    Args:
        page (int): There is a limit of translations displayed per page,
            and a page can be selected passing this URL argument.

    """
    page = request.args.get('page', 1, type=int)
    pagination = projections.get(page)

    logger.debug(f'processing GET "/json", page:{page}/{pagination.last_page}')

    translations_json = jsonify([{
        'text':
        translation.text or '',
        'status':
        translation.status or '',
        'translated_text':
        translation.translated_text or ''
    } for translation in pagination.items])

    return translations_json
    def nmt_process(self, translation):
        """Automatically process translation and update its status.

        Args:
            translation (Translation): The translation to be processed.

        """
        text = translation.text

        logger.debug(f'Requesting automatic translation')

        try:
            translated_text = self._marian_client.request_translation(text)
        except:  # noqa: E722
            event = TranslationAborted.create(
                f'Marian-NMT server request failed.')
            translation.apply(event)
            logger.error(f'Marian-NMT server request failed.')

            return translation

        event = TranslationFinished.create(translated_text)
        translation.apply(event)

        return translation
Beispiel #4
0
def index():
    """Application main route.

    Responding to a GET request, it renders a single-page application
    with a form to input an English text to be translated and a table
    containing a list of previous translations ordered by the length
    or the translated text.

    Responding to a POST request it validates if the form (avoiding a
    blank input), creates a new Translation aggregate with the given
    input and then saves it to the repository to be processesd in the
    background (and re-processed if the application breaks for some
    reason before enqueuing the task).

    Args:
        page (int): There is a limit of translations displayed per page,
            and a page can be selected passing this URL argument.

    """
    form = TranslationForm()

    if form.validate_on_submit():
        # POST handling
        logger.debug(f'processing POST "/"')

        text = form.text.data
        text = text.replace("'", "\\'")
        text = text.replace('\r', '')
        text = text.replace('\n', '')
        translation = Translation.create(text)

        repository.save(translation)

        method = form.method.data
        if method == 'mt':
            tasks.nmt_task.send(translation.id)
        else:
            tasks.translation_task.send(translation.id)
        tasks.projections_task.send(translation.id)

        return redirect(url_for('index'))

    # GET handling
    page = request.args.get('page', 1, type=int)
    translations = projections.get(page)
    logger.debug(
        f'processing GET "/json", page:{page}/{translations.last_page}')

    next_url = url_for('index', page=translations.next_page) \
        if translations.has_next else None
    prev_url = url_for('index', page=translations.prev_page) \
        if translations.has_prev else None

    return render_template('index.html',
                           form=form,
                           translations=translations.items,
                           next_url=next_url,
                           prev_url=prev_url)
    def insert_or_update(self,
                         session,
                         aggregate_uuid,
                         status,
                         original_text,
                         translated_text=None):
        """Insert or update an aggregate in the "translations" table.

        Args:
            session (Session): A session context into which operate.
            aggregate_uuid (str): An UUID4 string. It must match its
                respective event sourced aggregate.
            text (str): The text sent to the translation service.
            status (str): The translation processing status.
            translated_text (str): The text received from the
                translation service.

        """
        if status == 'requested':
            sql = text(
                f'INSERT INTO translations (uuid, status, length, text) '
                f'VALUES (:uuid, :status, :length, :text) ON CONFLICT DO NOTHING'
            )
            values = {
                'uuid': aggregate_uuid,
                'status': 'requested',
                'length': 0,
                'text': original_text
            }

        elif status == 'pending':
            sql = text(f"UPDATE translations "
                       f"SET status = 'pending' "
                       f"WHERE uuid = :aggregate_uuid")
            values = {'aggregate_uuid': aggregate_uuid}

        elif status == 'finished':
            sql = text(f"UPDATE translations "
                       f"SET status = 'finished', "
                       f"length = {len(translated_text)}, "
                       f"translated_text = :translated_text "
                       f"WHERE uuid = :aggregate_uuid")
            values = {
                'aggregate_uuid': aggregate_uuid,
                'translated_text': translated_text
            }
        elif status == 'aborted':
            sql = text(f"UPDATE translations "
                       f"SET status = 'aborted' "
                       f"WHERE uuid = :aggregate_uuid")
            values = {'aggregate_uuid': aggregate_uuid}

        logger.debug(sql)
        session.execute(sql, values)
Beispiel #6
0
    def trigger(self, event):
        """Trigger an event action to the aggregate.

        It uses the `singledispatch` decorator to decide which action
        to take based on the translation Event class input.

        Args:
            event (Event): An event.

        """

        @singledispatch
        def _trigger(_event):
            raise InvalidEventError(f'Invalid event: {_event}')

        @_trigger.register(TranslationRequested)
        def _(_event):
            if self.status != '':
                raise InvalidStatusError(
                    f'Invalid status transition: {self.status}->requested')
            self.text = _event.text
            self.status = 'requested'

        @_trigger.register(TranslationPending)
        def _(_event):
            if self.status != 'requested':
                raise InvalidStatusError(
                    f'Invalid status transition: {self.status}->pending')
            self.translation_id = _event.translation_id
            self.status = 'pending'

        @_trigger.register(TranslationFinished)
        def _(_event):
            if self.status != 'pending':
                raise InvalidStatusError(
                    f'Invalid status transition: {self.status}->finished')
            self.translated_text = _event.translated_text
            self.status = 'finished'
            self.finished = True

        @_trigger.register(TranslationAborted)
        def _(_event):
            self.error = _event.error
            self.status = 'aborted'
            self.finished = True

        logger.info(f'applying event ({event.id}) on aggregate: {self.id}')
        logger.debug(event.as_dict())

        _trigger(event)
Beispiel #7
0
def callback(id=None):
    """Callback route to update a Translation resource.

    This route is used by an external translation service to notify it
    finished processing a text.

    Args:
        id (str): The Translation aggregate ID to update it.

    """
    if id:
        logger.debug(f'processing POST "/callback/{id}"')
        translation = repository.get(id)
        translation = translator.get(translation)
        repository.save(translation)
        tasks.projections_task.send(id)
        return jsonify(success=True)

    return jsonify(error=404, text="Resource not found.")
    def get(self, translation):
        """Get a requested translation (and update it if required).

        If the request resource status is different from the local
        Translation's, the local translation resource is updated
        accordingly.

        Args:
            translation (Translation): The translation to be get (and
                updated).

        """
        logger.debug(f'Requesting translation {translation.id}:'
                     f'{translation.translation_id}')
        response = self._unbabel_client.get_translation(
            translation.translation_id)

        # Check for a response
        if response:
            uid = response['uid']
            status = response['status']

            # Check if status changed
            if (translation.status == status):
                return translation

            logger.debug(f'Updating translation {translation.id}:'
                         f'{translation.translation_id}')

            # Check wheter update a finished translation
            if status in _translation_finished:
                event = TranslationFinished.create(response['translatedText'])
                translation.apply(event)
            # Check wheter update a pending translation
            elif status in _translation_pending:
                event = TranslationPending.create(uid)
                translation.apply(event)
            # Check any other status unknown
            else:
                event = TranslationAborted.create(
                    f'Translation error: {status}')
                translation.apply(event)

        else:
            # Missing response
            logger.debug(f"Client returned an error: not found")
            event = TranslationAborted.create(f'Translation not found.')
            translation.apply(event)

        return translation
    def process(self, translation):
        """Process a requested translation and update its status.

        As its through an external service, it changes the Translation
        resource status to "pending" after snding a request to the
        Unbabel's API.

        Args:
            translation (Translation): The translation to be processed.

        """
        text = translation.text
        callback_url = urljoin(settings.API_CALLBACK, translation.id)

        logger.debug(
            f"Requesting translation with callback to '{callback_url}'")

        response = self._client.request_translation(text,
                                                    self._source_language,
                                                    self._target_language,
                                                    callback_url)

        # Check for request errors
        if 'error' in response.keys():
            event = TranslationAborted.create(
                f"Request error: {response['error']}")
            translation.apply(event)

            logger.debug(f"Client returned an error: {response['error']}")

            return translation

        uid = response['uid']
        status = response['status']

        logger.debug(f'Updating translation {translation.id}:{uid}')

        if status in _translation_pending:
            event = TranslationPending.create(uid)
            translation.apply(event)
        else:
            event = TranslationAborted.create(f'Translation error: {status}')
            translation.apply(event)

        return translation
Beispiel #10
0
    def append_to_stream(self, session, aggregate_uuid, expected_version,
                         events):
        """Append an event stream from an aggregate.

        Args:
            session (Session): A session context into which operate.
            aggregate_uuid (str): An UUID4 string. The aggregate from
                which query the events.
            expected_version (int): Version for optimistic lock. Before
                appending an event to an event stream, its aggregate
                must be loaded to avoid inconsistencies. Then, the
                `expected_version` is set and verified to make sure no
                other session will be able to alter the same aggregate
                at the same time.
            events ([Event]): List of events to append to the event
                stream. They will be converted to a dict formatted as a
                JSON before the operation.

        """
        if expected_version:
            # If an `expected_version` is given, the aggregate must be
            # updated as it is already in some version.
            sql = text(f"UPDATE aggregates "
                       f"SET version = :expected_version + 1 "
                       f"WHERE version = :expected_version "
                       f"AND uuid = :aggregate_uuid")
            values = {
                'expected_version': expected_version,
                'aggregate_uuid': aggregate_uuid
            }

            logger.debug(sql, values)
            result = session.execute(sql, values)

            if result.rowcount != 1:
                raise ConcurrencyError(
                    'Failed to update aggregate in database.')

        else:
            # Or else it's a new aggregate.
            sql = text(f"INSERT INTO aggregates (uuid, version) "
                       f"VALUES (:aggregate_uuid, 1)")
            values = {'aggregate_uuid': aggregate_uuid}

            logger.debug(sql)
            result = session.execute(sql, values)

            if result.rowcount != 1:
                raise WriteError('Failed to insert aggregate into database.')

        for event in events:
            # Iterate through events and trying to add them to "events" table,
            # relating each and every one with the same aggregate. But, in the
            # occasion an event is already there (based on its UUID column),
            # it's dropped (`DO NOTHING`).

            sql = text(
                f"INSERT INTO events (uuid, aggregate_uuid, event, data) "
                f"VALUES (:uuid,:aggregate_uuid,:event,:data) ON CONFLICT (uuid) DO NOTHING"
            )
            values = {
                'uuid': event.id,
                'aggregate_uuid': aggregate_uuid,
                'event': event.__class__.__name__,
                'data': json.dumps(event.as_dict())
            }

            result = session.execute(sql, values)

            logger.debug(sql)
            if result.rowcount:
                logger.info(f'new event: {event.id}')
            else:
                logger.debug(f'no new event')