diff --git a/access_controller/settings.py b/access_controller/settings.py index 55af7a5..6293efc 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -11,6 +11,8 @@ https://docs.djangoproject.com/en/3.1/ref/settings/ """ import os from pathlib import Path + + from dotenv import load_dotenv # Build paths inside the project like this: BASE_DIR / 'subdir'. diff --git a/docs/source/code.rst b/docs/source/code.rst index 1f0bd15..b07c93c 100644 --- a/docs/source/code.rst +++ b/docs/source/code.rst @@ -49,3 +49,33 @@ Views :members: +***************** +Обработка тикетов +***************** + +.. automodule:: main.requester + :members: + + +********************* +Обработка статистики +********************* + +.. automodule:: main.statistic_data + :members: + + +********************************* +Функционал администратора Zendesk +********************************* + +.. automodule:: main.zendesk_admin + :members: + + +******** +Тесты +******** + +.. automodule:: main.tests + :members: diff --git a/main/apiauth.py b/main/apiauth.py index b6488ba..c82d797 100644 --- a/main/apiauth.py +++ b/main/apiauth.py @@ -12,10 +12,11 @@ def api_auth() -> dict: Функция создания пользователя с использованием Zendesk API. Получает из env Zendesk - email, token, password пользователя. + Если данные валидны и пользователь Zendesk с указанным email и токеном или паролем существует, создается словарь данных пользователя, полученных через API c Zendesk. - :return: данные пользователя + :return: данные пользователя в виде словаря: id, имя, email, роль, аватар """ credentials = { 'subdomain': ACTRL_ZENDESK_SUBDOMAIN diff --git a/main/extra_func.py b/main/extra_func.py index 880daba..1c87f65 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,9 +1,9 @@ """ -Вспомогательные функции со списками пользователей, статистикой и т.д. +Вспомогательные функции. """ import logging from datetime import timedelta -from typing import Union +from typing import Union, Optional from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist @@ -55,7 +55,8 @@ def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) - Функция устанавливает пользователю роль легкого агента. :param user_profile: Профиль пользователя - :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" + :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent". + Предварительно снимаем тикеты, находящие в работы у пользователя. """ tickets: SearchResultGenerator = get_tickets_list(user_profile.user.email) ticket: ZenpyTicket @@ -85,7 +86,7 @@ def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) - def get_users_list() -> list: """ - Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. + Функция возвращает список пользователей Zendesk, относящихся к организации SYSTEM. """ zendesk = zenpy @@ -95,23 +96,29 @@ def get_users_list() -> list: return users -def get_tickets_list(email) -> list: +def get_tickets_list(email: str) -> Optional[list]: """ - Функция возвращает список тикетов пользователя Zendesk + Функция возвращает список тикетов пользователя Zendesk. + + :param email: Email пользователя + :return: Список тикетов пользователя """ return TicketListRequester().get_tickets_list_for_user(zenpy.get_user(email)) -def get_tickets_list_for_group(group_name): +def get_tickets_list_for_group(group_name: str) -> Optional[list]: """ - Функция возвращает список не назначенных, нерешённых тикетов группы Zendesk + Функция возвращает список не назначенных, не решённых тикетов группы Zendesk. + + :param group_name: Название группы пользователя + :return: Список тикетов группы """ return TicketListRequester().get_tickets_list_for_group(zenpy.get_group(group_name)) def update_profile(user_profile: UserProfile) -> None: """ - Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. + Функция обновляет профиль пользователя в БД в соответствии с текущим в Zendesk. :param user_profile: Профиль пользователя :return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя @@ -148,6 +155,9 @@ def check_user_auth(email: str, password: str) -> bool: """ Функция проверяет, верны ли входные данные. + :param email: Email пользователя + :param password: Пароль пользователя + :return: Существует ли пользователь :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован """ creds = { @@ -165,7 +175,7 @@ def check_user_auth(email: str, password: str) -> bool: def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None: """ - Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk. + Функция обновляет профиль пользователя в модели при изменении данных пользователя на Zendesk. :param profile: Профиль пользователя :param zendesk_user: Данные пользователя в Zendesk @@ -181,7 +191,10 @@ def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None: def count_users(users: list) -> tuple: """ - Функция подсчета количества сотрудников с ролями engineer и light_agent + Функция подсчета количества сотрудников с ролями engineer и light_agent. + + :param users: Список пользователей + :return: Количество инженеров, количество light_agents """ engineers, light_agents = 0, 0 for user in users: @@ -194,7 +207,7 @@ def count_users(users: list) -> tuple: def update_users_in_model() -> list: """ - Обновляет пользователей в модели UserProfile по списку пользователей в организации + Обновляет пользователей в модели UserProfile по списку пользователей в организации. """ users = get_users_list() for user in users: @@ -253,7 +266,13 @@ class DatabaseHandler(logging.Handler): def __init__(self): logging.Handler.__init__(self) - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: + """ + Функция записи в базу данных лога с изменением роли пользователя. + + :param record: Лог смены роли пользователя + :return: Запись в БД лога по смене роли пользователя с указанием новой и старой роли, а также автора изменения + """ database = RoleChangeLogs() users = record.msg if users[1]: @@ -284,7 +303,7 @@ class CsvFormatter(logging.Formatter): """ Функция форматирует запись смены роли пользователя в строку. - :param record: Запись смены роли пользователя. + :param record: Лог смены роли пользователя. :return: Строка с записью смены пользователя. """ users = record.msg @@ -307,7 +326,7 @@ class CsvFormatter(logging.Formatter): return msg -def log(user, admin=None): +def log(user: get_user_model(), admin: get_user_model() = None) -> None: """ Функция осуществляет запись логов в базу данных и csv файл. @@ -335,7 +354,7 @@ def set_session_params_for_work_page(request: WSGIRequest, count: int = None, is :param request: Получение данных с рабочей страницы пользователя :param count: Количество запрошенных тикетов - :param is_confirm: Назначение тикетов + :param is_confirm: Назначены ли тикеты :return: Перезагрузка страницы "Управление правами" соответствующего пользователя """ request.session['is_confirm'] = is_confirm diff --git a/main/forms.py b/main/forms.py index 929f3d9..e4db54a 100644 --- a/main/forms.py +++ b/main/forms.py @@ -1,5 +1,5 @@ """ -Формы. +Формы, использующиеся в приложении. """ from django import forms from django.contrib.auth.forms import AuthenticationForm diff --git a/main/models.py b/main/models.py index c934ab1..790a322 100644 --- a/main/models.py +++ b/main/models.py @@ -13,7 +13,6 @@ from access_controller.settings import ZENDESK_ROLES class UserProfile(models.Model): """ Модель профиля пользователя. - Профиль создается и изменяется при создании и изменении модель User. """ @@ -31,11 +30,7 @@ class UserProfile(models.Model): @property def zendesk_role(self) -> str: """ - Функция возвращает роль пользователя в Zendesk. - - В формате str, либо UNDEFINED, если пользователь не найден - - :return: Роль пользователя в Zendesk + Роль пользователя в Zendesk, либо UNDEFINED, если пользователь не найден. """ for role, r_id in ZENDESK_ROLES.items(): if r_id == self.custom_role_id: @@ -44,12 +39,12 @@ class UserProfile(models.Model): @receiver(post_save, sender=get_user_model()) -def create_user_profile(instance, created, **kwargs) -> None: +def create_user_profile(instance: get_user_model(), created: bool, **kwargs) -> None: """ Функция создания профиля пользователя (Userprofile) при регистрации пользователя. :param instance: Экземпляр класса User - :param created: Создание профиля пользователя + :param created: Существует ли пользователь :param kwargs: Параметры :return: Обновленный список объектов профилей пользователей """ @@ -58,7 +53,7 @@ def create_user_profile(instance, created, **kwargs) -> None: @receiver(post_save, sender=get_user_model()) -def save_user_profile(instance, **kwargs) -> None: +def save_user_profile(instance: get_user_model(), **kwargs) -> None: """ Функция записи БД профиля пользователя. @@ -84,7 +79,7 @@ class RoleChangeLogs(models.Model): class UnassignedTicketStatus(models.IntegerChoices): """ - Класс статусов не распределенных тикетов. + Модель статусов нераспределенных тикетов. :param UNASSIGNED: Снят с пользователя, перенесён в буферную группу :param RESTORED: Авторство восстановлено @@ -95,7 +90,7 @@ class UnassignedTicketStatus(models.IntegerChoices): """ UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу' RESTORED = 1, 'Авторство восстановлено' - NOT_FOUND = 2, 'Пока нас не было, тикет испарился из ' \ + NOT_FOUND = 2, 'Пока нас не было, тикет был перенесен из ' \ 'буферной группы. Дополнительные действия не требуются' CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются' SOLVED = 4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL' diff --git a/main/requester.py b/main/requester.py index d0c57ed..f5ba9e4 100644 --- a/main/requester.py +++ b/main/requester.py @@ -1,6 +1,8 @@ """ -Обработка тикетов. +Обработка тикетов, составление списков тикетов для пользователя и группы пользователей. """ +from typing import Optional + import requests from zenpy import TicketApi from zenpy.lib.api_objects import Ticket @@ -11,6 +13,13 @@ from main.zendesk_admin import zenpy class TicketListRequester: """ Класс обработки тикетов. + + :param email: Email пользователя + :type display: :class:`str` + :param token_or_password: Токен или пароль + :type display: :class:`str` + :param prefix: Формат строка url страницы Zendesk + :type display: :class:`str` """ def __init__(self): self.email = zenpy.credentials['email'] @@ -21,16 +30,22 @@ class TicketListRequester: self.token_or_password = zenpy.credentials.get('password') self.prefix = f'https://{zenpy.credentials.get("subdomain")}.zendesk.com/api/v2/' - def get_tickets_list_for_user(self, zendesk_user: zenpy) -> str: + def get_tickets_list_for_user(self, zendesk_user: zenpy) -> Optional[list]: """ Функция получения списка тикетов пользователя Zendesk. + + :param zendesk_user: Пользователь Zendesk + :return: Список тикетов, назначенных на данного пользователя в Zendesk """ url = self.prefix + f'users/{zendesk_user.id}/tickets/assigned' return self._get_tickets(url) - def get_tickets_list_for_group(self, group: zenpy) -> list(): + def get_tickets_list_for_group(self, group: zenpy) -> Optional[list]: """ Функция получения списка тикетов группы пользователей Zendesk. + + :param group: Название группы + :return: Список тикетов """ url = self.prefix + '/tickets' all_tickets = self._get_tickets(url) @@ -40,9 +55,12 @@ class TicketListRequester: tickets.append(ticket) return tickets - def _get_tickets(self, url: str) -> list(): + def _get_tickets(self, url: str) -> Optional[list]: """ Функция получения полного списка тикетов по url. + + :param url: Url Zendesk c указанием тикетов, назначенных на пользователя + :return: Список тикетов """ response = requests.get(url, auth=(self.email, self.token_or_password)) tickets = [] diff --git a/main/serializers.py b/main/serializers.py index 70c4352..00b85a6 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -1,5 +1,5 @@ """ -Сериализаторы. +Сериализаторы, используемые в приложении. """ from django.contrib.auth import get_user_model from rest_framework import serializers diff --git a/main/views.py b/main/views.py index 1369d94..e58533c 100644 --- a/main/views.py +++ b/main/views.py @@ -61,7 +61,7 @@ def setup_context(**kwargs) -> Dict[str, Any]: class CustomRegistrationView(RegistrationView): """ - Отображение и логика работы страницы регистрации пользователя. + Класс отображения и логики работы страницы регистрации пользователя. :param form_class: Форма, которую необходимо заполнить для регистрации :type form_class: :class:`forms.CustomRegistrationForm` @@ -86,9 +86,12 @@ class CustomRegistrationView(RegistrationView): def register(self, form: CustomRegistrationForm) -> Optional[get_user_model()]: """ Функция регистрации пользователя. - 1. Ввод email пользователя, указанный на Zendesk + + 1. Ввод email пользователя, указанный на Zendesk. + 2. В случае если пользователь с данным паролем зарегистрирован на Zendesk и относится к организации SYSTEM, - происходит сброс ссылки с установлением пароля на указанный email + происходит сброс ссылки с установлением пароля на указанный email. + 3. Создается пользователь class User, а также его профиль. :param form: Email пользователя на Zendesk @@ -133,7 +136,7 @@ class CustomRegistrationView(RegistrationView): """ Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin. - :param user: авторизованный пользователь (получает разрешение, имея роль "admin") + :param user: Авторизованный пользователь (получает разрешение, имея роль "admin") """ if user.userprofile.role == 'admin': content_type = ContentType.objects.get_for_model(UserProfile) @@ -148,8 +151,8 @@ class CustomRegistrationView(RegistrationView): Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации. Используется самой django-registration. - :param user: пользователь, пытающийся зарегистрироваться - :return: адресация на страницу успешной регистрации + :param user: Пользователь, пытающийся зарегистрироваться + :return: Адресация на страницу успешной регистрации """ return self.urls[self.redirect_url] @@ -158,8 +161,8 @@ def registration_error(request: WSGIRequest) -> HttpResponse: """ Функция отображения страницы ошибки регистрации. - :param request: регистрация - :return: адресация на страницу ошибки + :param request: Регистрация + :return: Адресация на страницу ошибки """ return render(request, 'django_registration/registration_error.html') @@ -169,8 +172,8 @@ def profile_page(request: WSGIRequest) -> HttpResponse: """ Функция отображения страницы профиля. - :param request: данные пользователя из БД - :return: адресация на страницу пользователя + :param request: Данные пользователя из БД + :return: Адресация на страницу пользователя """ user_profile: UserProfile = request.user.userprofile update_profile(user_profile) @@ -187,9 +190,9 @@ def work_page(request: WSGIRequest, required_id: int) -> HttpResponse: """ Функция отображения страницы "Управления правами" для текущего пользователя (login_required). - :param request: объект пользователя + :param request: Объект пользователя :param id: id пользователя, используется для динамической адресации - :return: адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают + :return: Адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают """ users = get_users_list() if request.user.id == required_id: @@ -227,8 +230,8 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: """ Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent" - :param request: данные текущего пользователя (login_required) - :return: перезагрузка текущей страницы после выполнения смены роли + :param request: Данные текущего пользователя (login_required) + :return: Перезагрузка текущей страницы после выполнения смены роли """ make_light_agent(request.user.userprofile, request.user) return set_session_params_for_work_page(request) @@ -240,8 +243,8 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на "engineer". - :param request: данные текущего пользователя (login_required) - :return: перезагрузка текущей страницы после выполнения смены роли + :param request: Данные текущего пользователя (login_required) + :return: Перезагрузка текущей страницы после выполнения смены роли """ make_engineer(request.user.userprofile, request.user) return set_session_params_for_work_page(request) @@ -250,9 +253,10 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: @login_required() def work_get_tickets(request: WSGIRequest) -> HttpResponse: """ + Функция получения тикетов в работу. - :param request: - :return: + :param request: Запрос на принятие тикетов в работу + :return: Перезагрузка рабочей страницы """ zenpy_user = zenpy.get_user(request.user.email) @@ -289,6 +293,8 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM :type form_class: :class:`forms.AdminPageUsersForm` :param success_url: Адрес страницы администратора :type success_url: :class:`HttpResponseRedirect` + :param success_message: Уведомление об изменении прав + :type success_url: :class:`str` """ permission_required = 'main.has_control_access' template_name = 'pages/adm_ruleset.html' @@ -333,7 +339,12 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM class CustomLoginView(LoginView): """ - Отображение страницы авторизации пользователя + Класс отображения страницы авторизации пользователя. + + :param extra_context: Добавление в контекст статус пользователя "залогинен" + :type extra_context: :class:`dict` + :param form_class: Форма страницы авторизации + :type form_class: :class: forms.CustomAuthenticationForm """ extra_context = setup_context(login_lit=True) form_class = CustomAuthenticationForm @@ -353,7 +364,8 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet): def list(self, request: WSGIRequest, *args, **kwargs) -> Response: """ - Функция возвращает список пользователей, список пользователей Zendesk, количество engineers и light-agents. + Функция возвращает список пользователей Zendesk, количество engineers и light-agents. + :param request: Запрос :param args: Аргументы :param kwargs: Параметры @@ -376,6 +388,7 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet): def choose_users(zendesk: list, model: list) -> list: """ Функция формирует список пользователей, которые не зарегистрированы у нас. + :param zendesk: Список пользователей Zendesk :param model: Список пользователей (модель Userprofile) :return: Список @@ -389,7 +402,8 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet): @staticmethod def get_zendesk_users(users: list) -> list: """ - Получение списка пользователей Zendesk, не являющихся админами. + Функция получения списка пользователей Zendesk, не являющихся админами. + :param users: Список пользователей :return: Список пользователей, не являющимися администраторами. """ @@ -406,8 +420,8 @@ def statistic_page(request: WSGIRequest) -> HttpResponse: """ Функция отображения страницы статистики (для "superuser"). - :param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm - :return: адресация на страницу статистики + :param request: Данные о пользователе: email, время и интервал работы. Данные получаем через forms.StFatisticForm + :return: Адресация на страницу статистики """ # if not request.user.has_perm('main.has_control_access'): diff --git a/venv/lib/python3.6/site-packages/enchant/tokenize/ru.py b/venv/lib/python3.6/site-packages/enchant/tokenize/ru.py deleted file mode 100644 index 7e15379..0000000 --- a/venv/lib/python3.6/site-packages/enchant/tokenize/ru.py +++ /dev/null @@ -1,185 +0,0 @@ -# pyenchant -# -# Copyright (C) 2004-2008, Ryan Kelly -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the -# Free Software Foundation, Inc., 59 Temple Place - Suite 330, -# Boston, MA 02111-1307, USA. -# -# In addition, as a special exception, you are -# given permission to link the code of this program with -# non-LGPL Spelling Provider libraries (eg: a MSFT Office -# spell checker backend) and distribute linked combinations including -# the two. You must obey the GNU Lesser General Public License in all -# respects for all of the code used other than said providers. If you modify -# this file, you may extend this exception to your version of the -# file, but you are not obligated to do so. If you do not wish to -# do so, delete this exception statement from your version. -# -""" - - enchant.tokenize.en: Tokenizer for the English language - - This module implements a PyEnchant text tokenizer for the English - language, based on very simple rules. - -""" - -import unicodedata - -import enchant.tokenize - - -class tokenize(enchant.tokenize.tokenize): # noqa: N801 - """Iterator splitting text into words, reporting position. - - This iterator takes a text string as input, and yields tuples - representing each distinct word found in the text. The tuples - take the form: - - (,) - - Where is the word string found and is the position - of the start of the word within the text. - - The optional argument may be used to specify a - list of additional characters that can form part of a word. - By default, this list contains only the apostrophe ('). Note that - these characters cannot appear at the start or end of a word. - """ - - _DOC_ERRORS = ["pos", "pos"] - - def __init__(self, text, valid_chars=None): - self._valid_chars = valid_chars - self._text = text - self._offset = 0 - # Select proper implementation of self._consume_alpha. - # 'text' isn't necessarily a string (it could be e.g. a mutable array) - # so we can't use isinstance(text, str) to detect unicode. - # Instead we typetest the first character of the text. - # If there's no characters then it doesn't matter what implementation - # we use since it won't be called anyway. - try: - char1 = text[0] - except IndexError: - self._initialize_for_binary() - else: - if isinstance(char1, str): - self._initialize_for_unicode() - else: - self._initialize_for_binary() - - def _initialize_for_binary(self): - self._consume_alpha = self._consume_alpha_b - if self._valid_chars is None: - self._valid_chars = ("'",) - - def _initialize_for_unicode(self): - self._consume_alpha = self._consume_alpha_u - if self._valid_chars is None: - # XXX TODO: this doesn't seem to work correctly with the - # MySpell provider, disabling for now. - # Allow unicode typographic apostrophe - # self._valid_chars = (u"'",u"\u2019") - self._valid_chars = ("'",) - - def _consume_alpha_b(self, text, offset): - """Consume an alphabetic character from the given bytestring. - - Given a bytestring and the current offset, this method returns - the number of characters occupied by the next alphabetic character - in the string. Non-ASCII bytes are interpreted as utf-8 and can - result in multiple characters being consumed. - """ - assert offset < len(text) - if text[offset].isalpha(): - return 1 - elif text[offset] >= "\x80": - return self._consume_alpha_utf8(text, offset) - return 0 - - def _consume_alpha_utf8(self, text, offset): - """Consume a sequence of utf8 bytes forming an alphabetic character.""" - incr = 2 - u = "" - while not u and incr <= 4: - try: - try: - # In the common case this will be a string - u = text[offset : offset + incr].decode("utf8") - except AttributeError: - # Looks like it was e.g. a mutable char array. - try: - s = text[offset : offset + incr].tostring() - except AttributeError: - s = "".join([c for c in text[offset : offset + incr]]) - u = s.decode("utf8") - except UnicodeDecodeError: - incr += 1 - if not u: - return 0 - if u.isalpha(): - return incr - if unicodedata.category(u)[0] == "M": - return incr - return 0 - - def _consume_alpha_u(self, text, offset): - """Consume an alphabetic character from the given unicode string. - - Given a unicode string and the current offset, this method returns - the number of characters occupied by the next alphabetic character - in the string. Trailing combining characters are consumed as a - single letter. - """ - assert offset < len(text) - incr = 0 - if text[offset].isalpha(): - incr = 1 - while offset + incr < len(text): - if unicodedata.category(text[offset + incr])[0] != "M": - break - incr += 1 - return incr - - def next(self): - text = self._text - offset = self._offset - while offset < len(text): - # Find start of next word (must be alpha) - while offset < len(text): - incr = self._consume_alpha(text, offset) - if incr: - break - offset += 1 - cur_pos = offset - # Find end of word using, allowing valid_chars - while offset < len(text): - incr = self._consume_alpha(text, offset) - if not incr: - if text[offset] in self._valid_chars: - incr = 1 - else: - break - offset += incr - # Return if word isn't empty - if cur_pos != offset: - # Make sure word doesn't end with a valid_char - while text[offset - 1] in self._valid_chars: - offset = offset - 1 - self._offset = offset - return (text[cur_pos:offset], cur_pos) - self._offset = offset - raise StopIteration()