From dd0791f5e8b62375dc252af2c5fe9100abe06f83 Mon Sep 17 00:00:00 2001 From: FlorianWilhelm Date: Thu, 28 Dec 2023 08:59:50 +0000 Subject: [PATCH] Fix unit test again on Github CICD c3ff79df516881dadf6863e87164aa04b876b967 --- dev/contributing/index.html | 2 +- dev/search/search_index.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/contributing/index.html b/dev/contributing/index.html index 19e1982..51eea8c 100644 --- a/dev/contributing/index.html +++ b/dev/contributing/index.html @@ -17,5 +17,5 @@ and start making changes. Never work on the main branch!

  • Start your work on this branch. Don't forget to add docstrings in Google style to new functions, modules and classes, especially if they are part of public APIs.

  • Add yourself to the list of contributors in AUTHORS.md.

  • When you’re done editing, do:

    git add <MODIFIED FILES>
     git commit
     
    to record your changes in git.
    Please make sure to see the validation messages from pre-commit and fix any eventual issues. This should automatically use flake8/black to check/fix the code style in a way that is compatible with the project.

    Info

    Don't forget to add unit tests and documentation in case your contribution adds a feature and is not just a bugfix.

    Moreover, writing an descriptive commit message is highly recommended. In case of doubt, you can check the commit history with:

    git log --graph --decorate --pretty=oneline --abbrev-commit --all
    -
    to look for recurring communication patterns.

  • Please check that your changes don't break any unit tests with hatch run test:cov or hatch run test:no-cov to run the unitest with or without coverage reports, respectively.

  • For code hygiene, execute hatch run lint:all in order to run flake8, isort, black, mypy, etc.
  • Submit your contribution

    1. If everything works fine, push your local branch to the remote server with:
    git push -u origin my-feature
    +
    to look for recurring communication patterns.

  • Please check that your changes don't break any unit tests with hatch run cov or hatch run no-cov to run the unitest with or without coverage reports, respectively.

  • For code hygiene, execute hatch run lint:all in order to run flake8, isort, black, mypy, etc.
  • Submit your contribution

    1. If everything works fine, push your local branch to the remote server with:
    git push -u origin my-feature
     
    1. Go to the web page of your fork and click "Create pull request" to send your changes for review.

    Find more detailed information in creating a PR. You might also want to open the PR as a draft first and mark it as ready for review after the feedbacks from the continuous integration (CI) system or any required fixes.


    1. Even though these resources focus on open source projects and communities, the general ideas behind collaborating with other developers to collectively create software are general and can be applied to all sorts of environments, including private companies and proprietary code bases. 

    \ No newline at end of file diff --git a/dev/search/search_index.json b/dev/search/search_index.json index 5760eac..27398b7 100644 --- a/dev/search/search_index.json +++ b/dev/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\u200b","text":"

    Pytanis includes a Pretalx client and all the tooling you need for conferences using Pretalx, from handling the initial call for papers to creating the final program.

    Trivia: The name Pytanis is a reference to Prytanis using the typical py prefix of Python tools. Prytanis was the name given to the leading members of the government of a city (polis) in ancient Greece. Offices that used this title usually had responsibility for presiding over councils of some kind, which met in the Prytaneion. Romani ite domum!

    "},{"location":"#features","title":"Features","text":""},{"location":"#license","title":"License","text":"

    Pytanis is distributed under the terms of the MIT license.

    "},{"location":"#navigation","title":"Navigation","text":"

    Documentation for specific MAJOR.MINOR versions can be chosen by using the dropdown on the top of every page. The dev version reflects changes that have not yet been released.

    Also, desktop readers can use special keyboard shortcuts:

    Keys Action Navigate to the \"previous\" page Navigate to the \"next\" page Display the search modal"},{"location":"authors/","title":"Authors","text":""},{"location":"authors/#contributors","title":"Contributors","text":""},{"location":"changelog/","title":"Changelog","text":""},{"location":"changelog/#changelog","title":"Changelog","text":""},{"location":"changelog/#version-07","title":"Version 0.7","text":""},{"location":"changelog/#version-061-2023-12-10","title":"Version 0.6.1 (2023-12-10)","text":""},{"location":"changelog/#version-06-2023-12-04","title":"Version 0.6 (2023-12-04)","text":""},{"location":"changelog/#version-05-2023-04-10","title":"Version 0.5 (2023-04-10)","text":""},{"location":"changelog/#version-041-2023-03-25","title":"Version 0.4.1 (2023-03-25)","text":""},{"location":"changelog/#version-04-2023-03-10","title":"Version 0.4 (2023-03-10)","text":""},{"location":"changelog/#version-03-2023-02-17","title":"Version 0.3 (2023-02-17)","text":""},{"location":"changelog/#version-02-2023-02-11","title":"Version 0.2 (2023-02-11)","text":""},{"location":"changelog/#version-011-2023-01-16","title":"Version 0.1.1 (2023-01-16)","text":""},{"location":"changelog/#version-01-2023-01-15","title":"Version 0.1 (2023-01-15)","text":""},{"location":"contributing/","title":"Contributing","text":""},{"location":"contributing/#contributing","title":"Contributing","text":"

    Welcome to the contributor guide of Pytanis.

    This document focuses on getting any potential contributor familiarized with the development processes, but other kinds of contributions are also appreciated.

    If you are new to using git or have never collaborated on a project previously, please have a look at contribution-guide.org. Other resources are also listed in the excellent guide created by Freecodecamp1.

    Please note: all users and contributors are expected to be open, considerate, reasonable, and respectful. When in doubt, Python Software Foundation's Code of Conduct is a good reference in terms of behavior guidelines.

    "},{"location":"contributing/#issue-reports","title":"Issue Reports","text":"

    If you experience bugs or general issues with Pytanis, please have a look at the issue tracker. If you don't see anything useful there, please feel free to file an issue report.

    Tip

    Please don't forget to include the closed issues in your search. Sometimes a solution will have been reported already and the problem is considered solved.

    New issue reports should include information about your programming environment (e.g., operating system, Python version) and steps to reproduce the problem. Please try also to simplify the reproduction steps to a very minimal example that still illustrates the problem you are facing. By removing other factors, you help us to identify the root cause of the issue.

    "},{"location":"contributing/#documentation-improvements","title":"Documentation improvements","text":"

    You can contribute to the documentation of Pytanis by making them more readable and coherent, or by adding missing information and correcting mistakes.

    The documentation uses mkdocs as its main documentation compiler. This means that the docs are kept in the same repository as the project code, and that any documentation update is done in the same way was a code contribution.

    Tip

    Please note that the GitHub web interface provides a quick way of propose changes in Pytanis' files. While this mechanism can be tricky for normal code contributions, it works perfectly fine for contributing to the docs, and can be quite handy.

    If you are interested in trying this method out, please navigate to the docs folder in the source repository, find which file you would like to propose changes and click in the little pencil icon at the top to open GitHub's code editor. Once you finish editing the file, please write a message in the form at the bottom of the page describing which changes have you made and what are the motivations behind them and submit your proposal.

    When working on documentation changes in your local machine, you can build and serve them using hatch with hatch run docs:build and hatch run docs:serve, respectively.

    "},{"location":"contributing/#code-contributions","title":"Code Contributions","text":""},{"location":"contributing/#submit-an-issue","title":"Submit an issue","text":"

    Before you work on any non-trivial code contribution it's best to first create a report in the issue tracker to start a discussion on the subject. This often provides additional considerations and avoids unnecessary work.

    "},{"location":"contributing/#clone-the-repository","title":"Clone the repository","text":"
    1. Create a user account on GitHub if you do not already have one.

    2. Fork the project repository: click on the Fork button near the top of the page. This creates a copy of the code under your account on GitHub.

    3. Clone this copy to your local disk:

      git clone git@github.com:YourLogin/pytanis.git\ncd pytanis\n

    4. Make sure hatch is installed using pipx:

      pipx install hatch\n

    5. [only once] install pre-commit hooks in the default environment with:

      hatch run pre-commit install\n

    "},{"location":"contributing/#implement-your-changes","title":"Implement your changes","text":"
    1. Create a branch to hold your changes:

      git checkout -b my-feature\n
      and start making changes. Never work on the main branch!

    2. Start your work on this branch. Don't forget to add docstrings in Google style to new functions, modules and classes, especially if they are part of public APIs.

    3. Add yourself to the list of contributors in AUTHORS.md.

    4. When you\u2019re done editing, do:

      git add <MODIFIED FILES>\ngit commit\n
      to record your changes in git. Please make sure to see the validation messages from pre-commit and fix any eventual issues. This should automatically use flake8/black to check/fix the code style in a way that is compatible with the project.

      Info

      Don't forget to add unit tests and documentation in case your contribution adds a feature and is not just a bugfix.

      Moreover, writing an descriptive commit message is highly recommended. In case of doubt, you can check the commit history with:

      git log --graph --decorate --pretty=oneline --abbrev-commit --all\n
      to look for recurring communication patterns.

    5. Please check that your changes don't break any unit tests with hatch run test:cov or hatch run test:no-cov to run the unitest with or without coverage reports, respectively.

    6. For code hygiene, execute hatch run lint:all in order to run flake8, isort, black, mypy, etc.
    "},{"location":"contributing/#submit-your-contribution","title":"Submit your contribution","text":"
    1. If everything works fine, push your local branch to the remote server with:
    git push -u origin my-feature\n
    1. Go to the web page of your fork and click \"Create pull request\" to send your changes for review.

    Find more detailed information in creating a PR. You might also want to open the PR as a draft first and mark it as ready for review after the feedbacks from the continuous integration (CI) system or any required fixes.

    1. Even though these resources focus on open source projects and communities, the general ideas behind collaborating with other developers to collectively create software are general and can be applied to all sorts of environments, including private companies and proprietary code bases.\u00a0\u21a9

    "},{"location":"license/","title":"License","text":"

    The MIT License (MIT)

    Copyright \u00a9 2023 Florian Wilhelm

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    "},{"location":"reference/SUMMARY/","title":"SUMMARY","text":""},{"location":"reference/pytanis/","title":"Reference","text":""},{"location":"reference/pytanis/#pytanis","title":"pytanis","text":""},{"location":"reference/pytanis/#pytanis.__all__","title":"__all__ = ['__version__', 'GSheetClient', 'PretalxClient', 'HelpDeskClient', 'get_cfg'] module-attribute","text":""},{"location":"reference/pytanis/#pytanis.__version__","title":"__version__ = version('pytanis') module-attribute","text":""},{"location":"reference/pytanis/#pytanis.GSheetClient","title":"GSheetClient(config: Config | None = None, *, read_only: bool = True)","text":"

    Google API to easily handle GSheets and other files on GDrive

    By default, only the least permissive scope GSHEET_RO in case of read_only = True is used.

    Source code in src/pytanis/google.py
    def __init__(self, config: Config | None = None, *, read_only: bool = True):\n    self._read_only = read_only\n    if read_only:\n        self._scopes = [Scope.GSHEET_RO]\n    else:\n        self._scopes = [Scope.GSHEET_RW]\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    self.gc = gspread_client(self._scopes, config)  # gspread client for more functionality\n
    "},{"location":"reference/pytanis/#pytanis.GSheetClient.gc","title":"gc = gspread_client(self._scopes, config) instance-attribute","text":""},{"location":"reference/pytanis/#pytanis.GSheetClient.clear_gsheet","title":"clear_gsheet(spreadsheet_id: str, worksheet_name: str)","text":"

    Clear the worksheet including values, formatting, filtering, etc.

    Source code in src/pytanis/google.py
    def clear_gsheet(self, spreadsheet_id: str, worksheet_name: str):\n    \"\"\"Clear the worksheet including values, formatting, filtering, etc.\"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name, create_ws=False)\n    default_fmt = get_default_format(worksheet.spreadsheet)\n    wrange = worksheet_range(worksheet)\n    try:\n        worksheet.clear()\n        worksheet.clear_basic_filter()\n        format_cell_range(worksheet, wrange, default_fmt)\n        rules = get_conditional_format_rules(worksheet)\n        rules.clear()\n        rules.save()\n        set_data_validation_for_cell_range(worksheet, wrange, None)\n    except APIError as error:\n        self._exception_feedback(error)\n
    "},{"location":"reference/pytanis/#pytanis.GSheetClient.gsheet","title":"gsheet(spreadsheet_id: str, worksheet_name: str | None = None, *, create_ws: bool = False) -> Worksheet | Spreadsheet","text":"

    Retrieve a Google sheet by its id and the name

    Open a Google sheet in your browser and check the URL to retrieve the id, e.g.: https://docs.google.com/spreadsheets/d/SPREEDSHEET_ID/edit...

    If the spreadsheet as several worksheets (check the lower bar) then worksheet_name can be used to specify a specific one.

    Source code in src/pytanis/google.py
    def gsheet(\n    self, spreadsheet_id: str, worksheet_name: str | None = None, *, create_ws: bool = False\n) -> Worksheet | Spreadsheet:\n    \"\"\"Retrieve a Google sheet by its id and the name\n\n    Open a Google sheet in your browser and check the URL to retrieve the id, e.g.:\n    https://docs.google.com/spreadsheets/d/SPREEDSHEET_ID/edit...\n\n    If the spreadsheet as several worksheets (check the lower bar) then `worksheet_name` can be used to\n    specify a specific one.\n    \"\"\"\n    spreadsheet = self.gc.open_by_key(spreadsheet_id)\n    if worksheet_name is None:\n        return spreadsheet\n    elif worksheet_name in [ws.title for ws in spreadsheet.worksheets()]:\n        return spreadsheet.worksheet(worksheet_name)\n    elif create_ws:\n        worksheet = spreadsheet.add_worksheet(title=worksheet_name, rows=100, cols=20)\n        self._wait_for_worksheet(spreadsheet_id, worksheet_name)\n        return worksheet\n    else:\n        return spreadsheet.worksheet(worksheet_name)  # raises exception\n
    "},{"location":"reference/pytanis/#pytanis.GSheetClient.gsheet_as_df","title":"gsheet_as_df(spreadsheet_id: str, worksheet_name: str, **kwargs: str | bool | int) -> pd.DataFrame","text":"

    Returns a worksheet as dataframe

    Source code in src/pytanis/google.py
    def gsheet_as_df(self, spreadsheet_id: str, worksheet_name: str, **kwargs: str | (bool | int)) -> pd.DataFrame:\n    \"\"\"Returns a worksheet as dataframe\"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name)\n    df = get_as_dataframe(worksheet, **kwargs)\n    # remove Nan rows & columns as they are exported by default\n    df.dropna(how='all', inplace=True, axis=0)\n    df.dropna(how='all', inplace=True, axis=1)\n    return df\n
    "},{"location":"reference/pytanis/#pytanis.GSheetClient.recreate_token","title":"recreate_token()","text":"

    Recreate the current token using the scopes given at initialization

    Source code in src/pytanis/google.py
    def recreate_token(self):\n    \"\"\"Recreate the current token using the scopes given at initialization\"\"\"\n    self._config.Google.token_json.unlink(missing_ok=True)\n    self.gc = gspread_client(self._scopes, self._config)\n
    "},{"location":"reference/pytanis/#pytanis.GSheetClient.save_df_as_gsheet","title":"save_df_as_gsheet(df: pd.DataFrame, spreadsheet_id: str, worksheet_name: str, *, create_ws: bool = False, default_fmt: bool = True, **kwargs: str | bool | int)","text":"

    Save the given dataframe as worksheet in a spreadsheet

    Make sure that the scope passed gives you write permissions

    Parameters:

    Name Type Description Default df DataFrame

    dataframe to save

    required spreadsheet_id str

    id of the Google spreadsheet

    required worksheet_name str

    name of the worksheet within the spreadsheet

    required create_ws bool

    create the worksheet if non-existent

    False default_fmt bool

    apply default formatter BasicFormatter

    True **kwargs str | bool | int

    extra keyword arguments passed to set_with_dataframe

    {} Source code in src/pytanis/google.py
    def save_df_as_gsheet(\n    self,\n    df: pd.DataFrame,\n    spreadsheet_id: str,\n    worksheet_name: str,\n    *,\n    create_ws: bool = False,\n    default_fmt: bool = True,\n    **kwargs: str | (bool | int),\n):\n    \"\"\"Save the given dataframe as worksheet in a spreadsheet\n\n    Make sure that the scope passed gives you write permissions\n\n    Args:\n        df: dataframe to save\n        spreadsheet_id: id of the Google spreadsheet\n        worksheet_name: name of the worksheet within the spreadsheet\n        create_ws: create the worksheet if non-existent\n        default_fmt: apply default formatter `BasicFormatter`\n        **kwargs: extra keyword arguments passed to `set_with_dataframe`\n    \"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name, create_ws=create_ws)\n    # make sure it's really only the dataframe, not some residue\n    self.clear_gsheet(spreadsheet_id, worksheet_name)\n    params = {'resize': True} | dict(**kwargs)  # set sane defaults\n    try:\n        set_with_dataframe(worksheet, df, **params)\n        if default_fmt:\n            format_with_dataframe(worksheet, df)\n    except APIError as error:\n        self._exception_feedback(error)\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient","title":"HelpDeskClient(config: Config | None = None)","text":"Source code in src/pytanis/helpdesk/client.py
    def __init__(self, config: Config | None = None):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    # Important: Always use a custom User-Agent, never a generic one.\n    # Generic User-Agents are filtered by helpdesk to reduce spam.\n    self._headers = {'User-Agent': 'Pytanis'}\n\n    self._get_throttled = self._get\n    self._post_throttled = self._post\n    self.set_throttling(calls=1, seconds=2)  # Helpdesk is really strange when it comes to this\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.create_ticket","title":"create_ticket(ticket: NewTicket)","text":"Source code in src/pytanis/helpdesk/client.py
    def create_ticket(self, ticket: NewTicket):\n    return self.post('tickets', data=ticket.model_dump())\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.get","title":"get(endpoint: str, params: QueryParams | None = None) -> JSON","text":"

    Retrieve data via throttled GET request and return the JSON

    Source code in src/pytanis/helpdesk/client.py
    def get(self, endpoint: str, params: QueryParams | None = None) -> JSON:\n    \"\"\"Retrieve data via throttled GET request and return the JSON\"\"\"\n    resp = self._get_throttled(endpoint, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.list_agents","title":"list_agents() -> list[Agent]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_agents(self) -> list[Agent]:\n    agents = self.get('agents')\n    if not isinstance(agents, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Agent.model_validate(dct) for dct in agents]\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.list_teams","title":"list_teams() -> list[Team]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_teams(self) -> list[Team]:\n    teams = self.get('teams')\n    if not isinstance(teams, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Team.model_validate(dct) for dct in teams]\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.post","title":"post(endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON","text":"Source code in src/pytanis/helpdesk/client.py
    def post(self, endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON:\n    resp = self._post_throttled(endpoint, data, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/helpdesk/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.debug('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n    self._post_throttled = throttle(calls, seconds)(self._post)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient","title":"PretalxClient(config: Config | None = None, *, blocking: bool = False)","text":"

    Client for the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def __init__(self, config: Config | None = None, *, blocking: bool = False):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    self._get_throttled = self._get\n    self.blocking = blocking\n    self.set_throttling(calls=2, seconds=1)  # we are nice by default and Pretalx doesn't allow many calls at once.\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.blocking","title":"blocking = blocking instance-attribute","text":""},{"location":"reference/pytanis/#pytanis.PretalxClient.answer","title":"answer(event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer","text":"

    Returns a specific answer

    Source code in src/pytanis/pretalx/client.py
    def answer(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer:  # noqa: A002\n    \"\"\"Returns a specific answer\"\"\"\n    return self._endpoint_id(Answer, event_slug, 'answers', id, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.answers","title":"answers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]","text":"

    Lists all answers and their details

    Source code in src/pytanis/pretalx/client.py
    def answers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]:\n    \"\"\"Lists all answers and their details\"\"\"\n    return self._endpoint_lst(Answer, event_slug, 'answers', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.event","title":"event(event_slug: str, *, params: QueryParams | None = None) -> Event","text":"

    Returns detailed information about a specific event

    Source code in src/pytanis/pretalx/client.py
    def event(self, event_slug: str, *, params: QueryParams | None = None) -> Event:\n    \"\"\"Returns detailed information about a specific event\"\"\"\n    endpoint = f'/api/events/{event_slug}/'\n    result = self._get_one(endpoint, params)\n    _logger.debug('result', resp=result)\n    return Event.model_validate(result)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.events","title":"events(*, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]","text":"

    Lists all events and their details

    Source code in src/pytanis/pretalx/client.py
    def events(self, *, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]:\n    \"\"\"Lists all events and their details\"\"\"\n    count, results = self._get_many('/api/events/', params)\n    events = iter(_logger.debug('result', resp=r) or Event.model_validate(r) for r in results)\n    return count, events\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.me","title":"me() -> Me","text":"

    Returns what Pretalx knows about myself

    Source code in src/pytanis/pretalx/client.py
    def me(self) -> Me:\n    \"\"\"Returns what Pretalx knows about myself\"\"\"\n    result = self._get_one('/api/me')\n    return Me.model_validate(result)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.question","title":"question(event_slug: str, id: int, *, params: QueryParams | None = None) -> Question","text":"

    Returns a specific question

    Source code in src/pytanis/pretalx/client.py
    def question(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Question:  # noqa: A002\n    \"\"\"Returns a specific question\"\"\"\n    return self._endpoint_id(Question, event_slug, 'questions', id, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.questions","title":"questions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]","text":"

    Lists all questions and their details

    Source code in src/pytanis/pretalx/client.py
    def questions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]:\n    \"\"\"Lists all questions and their details\"\"\"\n    return self._endpoint_lst(Question, event_slug, 'questions', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.review","title":"review(event_slug: str, id: int, *, params: QueryParams | None = None) -> Review","text":"

    Returns a specific review

    Source code in src/pytanis/pretalx/client.py
    def review(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Review:  # noqa: A002\n    \"\"\"Returns a specific review\"\"\"\n    return self._endpoint_id(Review, event_slug, 'reviews', id, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.reviews","title":"reviews(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]","text":"

    Lists all reviews and their details

    Source code in src/pytanis/pretalx/client.py
    def reviews(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]:\n    \"\"\"Lists all reviews and their details\"\"\"\n    return self._endpoint_lst(Review, event_slug, 'reviews', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.room","title":"room(event_slug: str, id: int, *, params: QueryParams | None = None) -> Room","text":"

    Returns a specific room

    Source code in src/pytanis/pretalx/client.py
    def room(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Room:  # noqa: A002\n    \"\"\"Returns a specific room\"\"\"\n    return self._endpoint_id(Room, event_slug, 'rooms', id, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.rooms","title":"rooms(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]","text":"

    Lists all rooms and their details

    Source code in src/pytanis/pretalx/client.py
    def rooms(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]:\n    \"\"\"Lists all rooms and their details\"\"\"\n    return self._endpoint_lst(Room, event_slug, 'rooms', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.info('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.speaker","title":"speaker(event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker","text":"

    Returns a specific speaker

    Source code in src/pytanis/pretalx/client.py
    def speaker(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker:\n    \"\"\"Returns a specific speaker\"\"\"\n    return self._endpoint_id(Speaker, event_slug, 'speakers', code, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.speakers","title":"speakers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]","text":"

    Lists all speakers and their details

    Source code in src/pytanis/pretalx/client.py
    def speakers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]:\n    \"\"\"Lists all speakers and their details\"\"\"\n    return self._endpoint_lst(Speaker, event_slug, 'speakers', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.submission","title":"submission(event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission","text":"

    Returns a specific submission

    Source code in src/pytanis/pretalx/client.py
    def submission(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission:\n    \"\"\"Returns a specific submission\"\"\"\n    return self._endpoint_id(Submission, event_slug, 'submissions', code, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.submissions","title":"submissions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]","text":"

    Lists all submissions and their details

    Source code in src/pytanis/pretalx/client.py
    def submissions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]:\n    \"\"\"Lists all submissions and their details\"\"\"\n    return self._endpoint_lst(Submission, event_slug, 'submissions', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.tag","title":"tag(event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag","text":"

    Returns a specific tag

    Source code in src/pytanis/pretalx/client.py
    def tag(self, event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag:\n    \"\"\"Returns a specific tag\"\"\"\n    return self._endpoint_id(Tag, event_slug, 'tags', tag, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.tags","title":"tags(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]","text":"

    Lists all tags and their details

    Source code in src/pytanis/pretalx/client.py
    def tags(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]:\n    \"\"\"Lists all tags and their details\"\"\"\n    return self._endpoint_lst(Tag, event_slug, 'tags', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.talk","title":"talk(event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk","text":"

    Returns a specific talk

    Source code in src/pytanis/pretalx/client.py
    def talk(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk:\n    \"\"\"Returns a specific talk\"\"\"\n    return self._endpoint_id(Talk, event_slug, 'talks', code, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.talks","title":"talks(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]","text":"

    Lists all talks and their details

    Source code in src/pytanis/pretalx/client.py
    def talks(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]:\n    \"\"\"Lists all talks and their details\"\"\"\n    return self._endpoint_lst(Talk, event_slug, 'talks', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.get_cfg","title":"get_cfg() -> Config","text":"

    Returns the configuration as an object

    Source code in src/pytanis/config.py
    def get_cfg() -> Config:\n    \"\"\"Returns the configuration as an object\"\"\"\n    cfg_path = get_cfg_file()\n    with open(cfg_path, 'rb') as fh:\n        cfg_dict = tomli.load(fh)\n    # add config path to later resolve relative paths of config values\n    cfg_dict['cfg_path'] = cfg_path\n    return Config.model_validate(cfg_dict)\n
    "},{"location":"reference/pytanis/config/","title":"Config","text":""},{"location":"reference/pytanis/config/#pytanis.config","title":"config","text":"

    Handling the configuration

    "},{"location":"reference/pytanis/config/#pytanis.config.PYTANIS_CFG_PATH","title":"PYTANIS_CFG_PATH: str = '.pytanis/config.toml' module-attribute","text":"

    Path within $HOME to the configuration file of Pytanis

    "},{"location":"reference/pytanis/config/#pytanis.config.PYTANIS_ENV","title":"PYTANIS_ENV: str = 'PYTANIS_CONFIG' module-attribute","text":"

    Name of the environment variable to look up the path for the config

    "},{"location":"reference/pytanis/config/#pytanis.config.Config","title":"Config","text":"

    Main configuration object

    "},{"location":"reference/pytanis/config/#pytanis.config.Config.Google","title":"Google: GoogleCfg instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.Config.HelpDesk","title":"HelpDesk: HelpDeskCfg instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.Config.Pretalx","title":"Pretalx: PretalxCfg instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.Config.cfg_path","title":"cfg_path: FilePath instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.Config.convert_json_path","title":"convert_json_path(v: GoogleCfg, info: FieldValidationInfo) -> GoogleCfg classmethod","text":"Source code in src/pytanis/config.py
    @field_validator('Google')\n@classmethod\ndef convert_json_path(cls, v: GoogleCfg, info: FieldValidationInfo) -> GoogleCfg:\n    def make_rel_path_abs(entry):\n        if entry is not None and not entry.is_absolute():\n            entry = info.data['cfg_path'].parent / entry\n        return entry\n\n    v.client_secret_json = make_rel_path_abs(v.client_secret_json)\n    v.token_json = make_rel_path_abs(v.token_json)\n\n    return v\n
    "},{"location":"reference/pytanis/config/#pytanis.config.GoogleCfg","title":"GoogleCfg","text":"

    Configuration related to the Google API

    "},{"location":"reference/pytanis/config/#pytanis.config.GoogleCfg.client_secret_json","title":"client_secret_json: Path | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.GoogleCfg.token_json","title":"token_json: Path | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.HelpDeskCfg","title":"HelpDeskCfg","text":"

    Configuration related to the HelpDesk API

    "},{"location":"reference/pytanis/config/#pytanis.config.HelpDeskCfg.account","title":"account: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.HelpDeskCfg.entity_id","title":"entity_id: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.HelpDeskCfg.token","title":"token: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.PretalxCfg","title":"PretalxCfg","text":"

    Configuration related to the Pretalx API

    "},{"location":"reference/pytanis/config/#pytanis.config.PretalxCfg.api_token","title":"api_token: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.get_cfg","title":"get_cfg() -> Config","text":"

    Returns the configuration as an object

    Source code in src/pytanis/config.py
    def get_cfg() -> Config:\n    \"\"\"Returns the configuration as an object\"\"\"\n    cfg_path = get_cfg_file()\n    with open(cfg_path, 'rb') as fh:\n        cfg_dict = tomli.load(fh)\n    # add config path to later resolve relative paths of config values\n    cfg_dict['cfg_path'] = cfg_path\n    return Config.model_validate(cfg_dict)\n
    "},{"location":"reference/pytanis/config/#pytanis.config.get_cfg_file","title":"get_cfg_file() -> Path","text":"

    Determines the path of the config file

    Source code in src/pytanis/config.py
    def get_cfg_file() -> Path:\n    \"\"\"Determines the path of the config file\"\"\"\n    path_str = os.environ.get(PYTANIS_ENV, None)\n    path = Path.home() / Path(PYTANIS_CFG_PATH) if path_str is None else Path(path_str)\n    return path\n
    "},{"location":"reference/pytanis/google/","title":"Google","text":""},{"location":"reference/pytanis/google/#pytanis.google","title":"google","text":"

    Functionality around the Google's Spreadsheet API

    Additional Documentation

    "},{"location":"reference/pytanis/google/#pytanis.google.ColorType","title":"ColorType = str | tuple[float, float, float] | tuple[float, float, float, float] module-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.__all__","title":"__all__ = ['GSheetClient', 'gsheet_rows_for_fmt', 'PermissionDeniedError'] module-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient","title":"GSheetClient(config: Config | None = None, *, read_only: bool = True)","text":"

    Google API to easily handle GSheets and other files on GDrive

    By default, only the least permissive scope GSHEET_RO in case of read_only = True is used.

    Source code in src/pytanis/google.py
    def __init__(self, config: Config | None = None, *, read_only: bool = True):\n    self._read_only = read_only\n    if read_only:\n        self._scopes = [Scope.GSHEET_RO]\n    else:\n        self._scopes = [Scope.GSHEET_RW]\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    self.gc = gspread_client(self._scopes, config)  # gspread client for more functionality\n
    "},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.gc","title":"gc = gspread_client(self._scopes, config) instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.clear_gsheet","title":"clear_gsheet(spreadsheet_id: str, worksheet_name: str)","text":"

    Clear the worksheet including values, formatting, filtering, etc.

    Source code in src/pytanis/google.py
    def clear_gsheet(self, spreadsheet_id: str, worksheet_name: str):\n    \"\"\"Clear the worksheet including values, formatting, filtering, etc.\"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name, create_ws=False)\n    default_fmt = get_default_format(worksheet.spreadsheet)\n    wrange = worksheet_range(worksheet)\n    try:\n        worksheet.clear()\n        worksheet.clear_basic_filter()\n        format_cell_range(worksheet, wrange, default_fmt)\n        rules = get_conditional_format_rules(worksheet)\n        rules.clear()\n        rules.save()\n        set_data_validation_for_cell_range(worksheet, wrange, None)\n    except APIError as error:\n        self._exception_feedback(error)\n
    "},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.gsheet","title":"gsheet(spreadsheet_id: str, worksheet_name: str | None = None, *, create_ws: bool = False) -> Worksheet | Spreadsheet","text":"

    Retrieve a Google sheet by its id and the name

    Open a Google sheet in your browser and check the URL to retrieve the id, e.g.: https://docs.google.com/spreadsheets/d/SPREEDSHEET_ID/edit...

    If the spreadsheet as several worksheets (check the lower bar) then worksheet_name can be used to specify a specific one.

    Source code in src/pytanis/google.py
    def gsheet(\n    self, spreadsheet_id: str, worksheet_name: str | None = None, *, create_ws: bool = False\n) -> Worksheet | Spreadsheet:\n    \"\"\"Retrieve a Google sheet by its id and the name\n\n    Open a Google sheet in your browser and check the URL to retrieve the id, e.g.:\n    https://docs.google.com/spreadsheets/d/SPREEDSHEET_ID/edit...\n\n    If the spreadsheet as several worksheets (check the lower bar) then `worksheet_name` can be used to\n    specify a specific one.\n    \"\"\"\n    spreadsheet = self.gc.open_by_key(spreadsheet_id)\n    if worksheet_name is None:\n        return spreadsheet\n    elif worksheet_name in [ws.title for ws in spreadsheet.worksheets()]:\n        return spreadsheet.worksheet(worksheet_name)\n    elif create_ws:\n        worksheet = spreadsheet.add_worksheet(title=worksheet_name, rows=100, cols=20)\n        self._wait_for_worksheet(spreadsheet_id, worksheet_name)\n        return worksheet\n    else:\n        return spreadsheet.worksheet(worksheet_name)  # raises exception\n
    "},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.gsheet_as_df","title":"gsheet_as_df(spreadsheet_id: str, worksheet_name: str, **kwargs: str | bool | int) -> pd.DataFrame","text":"

    Returns a worksheet as dataframe

    Source code in src/pytanis/google.py
    def gsheet_as_df(self, spreadsheet_id: str, worksheet_name: str, **kwargs: str | (bool | int)) -> pd.DataFrame:\n    \"\"\"Returns a worksheet as dataframe\"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name)\n    df = get_as_dataframe(worksheet, **kwargs)\n    # remove Nan rows & columns as they are exported by default\n    df.dropna(how='all', inplace=True, axis=0)\n    df.dropna(how='all', inplace=True, axis=1)\n    return df\n
    "},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.recreate_token","title":"recreate_token()","text":"

    Recreate the current token using the scopes given at initialization

    Source code in src/pytanis/google.py
    def recreate_token(self):\n    \"\"\"Recreate the current token using the scopes given at initialization\"\"\"\n    self._config.Google.token_json.unlink(missing_ok=True)\n    self.gc = gspread_client(self._scopes, self._config)\n
    "},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.save_df_as_gsheet","title":"save_df_as_gsheet(df: pd.DataFrame, spreadsheet_id: str, worksheet_name: str, *, create_ws: bool = False, default_fmt: bool = True, **kwargs: str | bool | int)","text":"

    Save the given dataframe as worksheet in a spreadsheet

    Make sure that the scope passed gives you write permissions

    Parameters:

    Name Type Description Default df DataFrame

    dataframe to save

    required spreadsheet_id str

    id of the Google spreadsheet

    required worksheet_name str

    name of the worksheet within the spreadsheet

    required create_ws bool

    create the worksheet if non-existent

    False default_fmt bool

    apply default formatter BasicFormatter

    True **kwargs str | bool | int

    extra keyword arguments passed to set_with_dataframe

    {} Source code in src/pytanis/google.py
    def save_df_as_gsheet(\n    self,\n    df: pd.DataFrame,\n    spreadsheet_id: str,\n    worksheet_name: str,\n    *,\n    create_ws: bool = False,\n    default_fmt: bool = True,\n    **kwargs: str | (bool | int),\n):\n    \"\"\"Save the given dataframe as worksheet in a spreadsheet\n\n    Make sure that the scope passed gives you write permissions\n\n    Args:\n        df: dataframe to save\n        spreadsheet_id: id of the Google spreadsheet\n        worksheet_name: name of the worksheet within the spreadsheet\n        create_ws: create the worksheet if non-existent\n        default_fmt: apply default formatter `BasicFormatter`\n        **kwargs: extra keyword arguments passed to `set_with_dataframe`\n    \"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name, create_ws=create_ws)\n    # make sure it's really only the dataframe, not some residue\n    self.clear_gsheet(spreadsheet_id, worksheet_name)\n    params = {'resize': True} | dict(**kwargs)  # set sane defaults\n    try:\n        set_with_dataframe(worksheet, df, **params)\n        if default_fmt:\n            format_with_dataframe(worksheet, df)\n    except APIError as error:\n        self._exception_feedback(error)\n
    "},{"location":"reference/pytanis/google/#pytanis.google.PermissionDeniedError","title":"PermissionDeniedError","text":"

    Error for APIError with status PERMISSION_DENIED

    Most likely thrown in cases when the scope is not GSHEET_RW or the token needs to be updated accordingly.

    "},{"location":"reference/pytanis/google/#pytanis.google.Scope","title":"Scope","text":""},{"location":"reference/pytanis/google/#pytanis.google.Scope.GDRIVE_FILE","title":"GDRIVE_FILE = 'https://www.googleapis.com/auth/drive.file' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.Scope.GDRIVE_RO","title":"GDRIVE_RO = 'https://www.googleapis.com/auth/drive.readonly' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.Scope.GDRIVE_RW","title":"GDRIVE_RW = 'https://www.googleapis.com/auth/drive' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.Scope.GSHEET_RO","title":"GSHEET_RO = 'https://www.googleapis.com/auth/spreadsheets.readonly' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.Scope.GSHEET_RW","title":"GSHEET_RW = 'https://www.googleapis.com/auth/spreadsheets' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.gsheet_col","title":"gsheet_col(idx: int) -> str","text":"

    Convert a column index to Google Sheet range notation, e.g. A, BE, etc.

    Source code in src/pytanis/google.py
    def gsheet_col(idx: int) -> str:\n    \"\"\"Convert a column index to Google Sheet range notation, e.g. A, BE, etc.\"\"\"\n    idx += 1\n    chars = []\n    while idx:\n        chars.append(string.ascii_uppercase[(idx % 26) - 1])\n        idx //= 27\n    return ''.join(chars[::-1])\n
    "},{"location":"reference/pytanis/google/#pytanis.google.gsheet_rows_for_fmt","title":"gsheet_rows_for_fmt(mask: pd.Series, n_cols: int) -> list[str]","text":"

    Get the Google Sheet row range specifications for formatting

    Source code in src/pytanis/google.py
    def gsheet_rows_for_fmt(mask: pd.Series, n_cols: int) -> list[str]:\n    \"\"\"Get the Google Sheet row range specifications for formatting\"\"\"\n    rows = pd.Series(np.argwhere(mask.to_numpy()).reshape(-1) + 2)  # +2 since 1-index and header\n    last_col = gsheet_col(n_cols - 1)  # last index\n    rows = rows.map(lambda x: f'A{x}:{last_col}{x}')\n    return rows.to_list()\n
    "},{"location":"reference/pytanis/google/#pytanis.google.gspread_client","title":"gspread_client(scopes: list[Scope], config: Config) -> gspread.client.Client","text":"

    Creates the GSheet client using our configuration

    Read GSpread for usage details

    Source code in src/pytanis/google.py
    def gspread_client(scopes: list[Scope], config: Config) -> gspread.client.Client:\n    \"\"\"Creates the GSheet client using our configuration\n\n    Read [GSpread](https://docs.gspread.org/) for usage details\n    \"\"\"\n    if (secret_path := config.Google.client_secret_json) is None:\n        msg = 'You have to set Google.client_secret_json in your config.toml!'\n        raise RuntimeError(msg)\n    if (token_path := config.Google.token_json) is None:\n        msg = 'You have to set Google.token_json in your config.toml!'\n        raise RuntimeError(msg)\n\n    gc = gspread.oauth(\n        scopes=[scope.value for scope in scopes],\n        credentials_filename=str(secret_path),\n        authorized_user_filename=str(token_path),\n    )\n    return gc\n
    "},{"location":"reference/pytanis/google/#pytanis.google.mark_rows","title":"mark_rows(worksheet, mask: pd.Series, color: ColorType)","text":"

    Mark rows specified by a mask (condition) with a given color

    Color can be a tuple of RGB values or a Matplotlib string specification: https://matplotlib.org/stable/gallery/color/named_colors.html#css-colors

    Source code in src/pytanis/google.py
    def mark_rows(worksheet, mask: pd.Series, color: ColorType):\n    \"\"\"Mark rows specified by a mask (condition) with a given color\n\n    Color can be a tuple of RGB values or a Matplotlib string specification:\n    https://matplotlib.org/stable/gallery/color/named_colors.html#css-colors\n    \"\"\"\n    rows = gsheet_rows_for_fmt(mask, worksheet.col_count)\n    fmt = cellFormat(backgroundColor=Color(*to_rgb(color)))\n    if rows:\n        format_cell_ranges(worksheet, [(rng, fmt) for rng in rows])\n
    "},{"location":"reference/pytanis/google/#pytanis.google.worksheet_range","title":"worksheet_range(worksheet: Worksheet) -> str","text":"

    Returns a range encompassing the whole worksheet

    Source code in src/pytanis/google.py
    def worksheet_range(worksheet: Worksheet) -> str:\n    \"\"\"Returns a range encompassing the whole worksheet\"\"\"\n    last_row = worksheet.row_count\n    last_col = gsheet_col(worksheet.col_count)\n    return f'A1:{last_col}{last_row}'\n
    "},{"location":"reference/pytanis/highs/","title":"Highs","text":""},{"location":"reference/pytanis/highs/#pytanis.highs","title":"highs","text":"

    Some helper functions for HiGHS (https://highs.dev/)

    pyomo and highspy need to be installed, consider pip install 'pytanis[all]'.

    ToDo

    "},{"location":"reference/pytanis/highs/#pytanis.highs.read_sol_file","title":"read_sol_file(file_name: str) -> Iterator[tuple[str, float]]","text":"

    Read a solution file from HiGHS solver with default output style

    We assume here that your variable names are alphanumeric!

    No underscores, no dashes, etc.!

    Source code in src/pytanis/highs.py
    def read_sol_file(file_name: str) -> Iterator[tuple[str, float]]:\n    \"\"\"Read a solution file from HiGHS solver with default output style\n\n    Attention: We assume here that your variable names are alphanumeric!\n               No underscores, no dashes, etc.!\n    \"\"\"\n    line_re = re.compile(r'(\\w+)(?:\\((\\w+)\\))?(_binary_indicator_var)? ([.\\w-]+)')\n\n    with open(file_name, encoding='utf8') as fh:\n        while True:\n            line = fh.readline()\n            if line.startswith('# Columns'):\n                break\n        for line in fh.readlines():\n            if line.startswith('#'):\n                break\n            if (match_obj := line_re.match(line.strip())) is None:\n                msg = f'Could not interpret line: {line}'\n                raise RuntimeError(msg)\n            else:\n                var_name, idx, binary, val = match_obj.groups()\n            val = float(val)\n            binary = binary.replace('_', '.', 1) if binary else ''\n\n            if idx is None:\n                yield f'{var_name}{binary}', val\n            else:\n                idx = idx.replace('_', ',')\n                yield f'{var_name}[{idx}]{binary}', val\n
    "},{"location":"reference/pytanis/highs/#pytanis.highs.set_solution_from_file","title":"set_solution_from_file(model: ConcreteModel, file_name: str)","text":"

    Given a HiGHS solution file set the variables of a Pyomo model accordingly.

    This is a workaround to set a Pyomo model's variables to the solution from a HiGHS solution file.

    Source code in src/pytanis/highs.py
    def set_solution_from_file(model: ConcreteModel, file_name: str):\n    \"\"\"Given a HiGHS solution file set the variables of a Pyomo model accordingly.\n\n    This is a workaround to set a Pyomo model's variables to the solution\n    from a HiGHS solution file.\n    \"\"\"\n    # just to initialize we read it in using HiGHS. The result is incorrect though,\n    # as the order of variables is mixed up quite often. We fix this below!\n    opt = Highs()\n    opt.set_instance(model)\n    opt._solver_model.readSolution(file_name, 0)\n    opt._sol = opt._solver_model.getSolution()\n    opt.load_vars()\n\n    # read the actual mapping of the variable names to the values\n    file_sol = dict(read_sol_file(file_name))\n\n    # overwrite the values of the variables again using the symbolic names from the file\n    for v, ref_info in opt._referenced_variables.items():\n        using_cons, using_sos, using_obj = ref_info\n        if using_cons or using_sos or (using_obj is not None):\n            var = opt._vars[v][0]\n            var.set_value(file_sol[var.name], skip_validation=True)\n
    "},{"location":"reference/pytanis/review/","title":"Review","text":""},{"location":"reference/pytanis/review/#pytanis.review","title":"review","text":"

    Tools related to assigning proposals to reviewers

    In Pretalx assignments can be done in two directions:

    1. Assign proposals to reviewers
    2. Assign reviewers to proposals

    We will always assume direction 1. in this file when we talk about an assignment. So in Operation Research-speak, resources get assigned tasks, not the other way around. The time needed for the task of reviewing a proposal is quite homogeneous while the number of reviews a single reviewer may highly vary. Also, we will rather use the name submission instead of proposal as this also reflects the naming of the Pretalx API.

    We follow the convention over configuration principle here and thus check out the Col class for the naming of columns.

    "},{"location":"reference/pytanis/review/#pytanis.review.Col","title":"Col","text":"

    Additional conventions used for reviews

    "},{"location":"reference/pytanis/review/#pytanis.review.Col.address_as","title":"address_as = 'Address as' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.all_proposals","title":"all_proposals = 'All Proposals' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.committee_contact","title":"committee_contact = 'Committee Contact' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.committee_member","title":"committee_member = 'Committee Member' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.curr_assignments","title":"curr_assignments = 'Current Assignments' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.done_nreviews","title":"done_nreviews = 'Done #Reviews' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.nassignments","title":"nassignments = '#Assignments' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.nvotes","title":"nvotes = '#Votes' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.pretalx_activated","title":"pretalx_activated = 'Pretalx activated' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.rem_nreviews","title":"rem_nreviews = 'Remaining #Reviews' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.target_nreviews","title":"target_nreviews = 'Target #Reviews' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.track_prefs","title":"track_prefs = 'Track Preferences' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.vote_score","title":"vote_score = 'Vote Score' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.read_assignment_as_df","title":"read_assignment_as_df(file_path: Path) -> pd.DataFrame","text":"

    Reads an assignment and returns a dataframe

    Source code in src/pytanis/review.py
    def read_assignment_as_df(file_path: Path) -> pd.DataFrame:\n    \"\"\"Reads an assignment and returns a dataframe\"\"\"\n    with open(file_path, encoding='utf8') as fh:\n        curr_assign = json.load(fh)\n    df = pd.DataFrame({k: [v] for k, v in curr_assign.items()})\n    df = df.T.rename_axis(index=Col.email).rename(columns={0: Col.curr_assignments}).reset_index()\n    return df\n
    "},{"location":"reference/pytanis/review/#pytanis.review.save_assignments_as_json","title":"save_assignments_as_json(df: pd.DataFrame, file_path: Path | str)","text":"

    Save the dataframe as proposal assignment JSON file

    Source code in src/pytanis/review.py
    def save_assignments_as_json(df: pd.DataFrame, file_path: Path | str):\n    \"\"\"Save the dataframe as proposal assignment JSON file\"\"\"\n    file_path = Path(file_path)\n    df = df.loc[:, [Col.email, Col.curr_assignments]]\n    json_dct = json.loads(df.set_index(Col.email).to_json())[Col.curr_assignments]\n    # prettify the json string for human-edit-ability if reviewers need to be dropped later\n    json_str = json.dumps(json_dct).replace('{', '{\\n').replace('], ', '],\\n').replace(']}', ']\\n}')\n    with open(file_path, 'w', encoding='utf8') as fh:\n        fh.write(json_str)\n
    "},{"location":"reference/pytanis/utils/","title":"Utils","text":""},{"location":"reference/pytanis/utils/#pytanis.utils","title":"utils","text":"

    Additional utilities

    "},{"location":"reference/pytanis/utils/#pytanis.utils.RT","title":"RT = TypeVar('RT') module-attribute","text":""},{"location":"reference/pytanis/utils/#pytanis.utils.implode","title":"implode(df: pd.DataFrame, cols: str | list[str]) -> pd.DataFrame","text":"

    The inverse of Pandas' explode

    Source code in src/pytanis/utils.py
    def implode(df: pd.DataFrame, cols: str | list[str]) -> pd.DataFrame:\n    \"\"\"The inverse of Pandas' explode\"\"\"\n    if not isinstance(cols, list):\n        cols = [cols]\n    orig_cols = df.columns\n    grp_cols = [col for col in df.columns if col not in cols]\n    df = df.groupby(grp_cols, group_keys=True, dropna=False).aggregate({col: lambda x: x.tolist() for col in cols})\n    df.reset_index(inplace=True)\n    df = df.loc[:, orig_cols]\n    return df\n
    "},{"location":"reference/pytanis/utils/#pytanis.utils.pretty_timedelta","title":"pretty_timedelta(seconds: int) -> str","text":"

    Converts timedelta in seconds to human-readable string

    Parameters:

    Name Type Description Default seconds int

    time delta in seconds

    required

    Returns:

    Type Description str

    timedelta as pretty string

    Source code in src/pytanis/utils.py
    def pretty_timedelta(seconds: int) -> str:\n    \"\"\"Converts timedelta in seconds to human-readable string\n\n    Args:\n        seconds: time delta in seconds\n\n    Returns:\n        timedelta as pretty string\n    \"\"\"\n    sign = '-' if seconds < 0 else ''\n    seconds = abs(int(seconds))\n    days, seconds = divmod(seconds, 86400)\n    hours, seconds = divmod(seconds, 3600)\n    minutes, seconds = divmod(seconds, 60)\n    if days > 0:\n        return f'{sign}{days}d{hours}h{minutes}m{seconds}s'\n    elif hours > 0:\n        return f'{sign}{hours}h{minutes}m{seconds}s'\n    elif minutes > 0:\n        return f'{sign}{minutes}m{seconds}s'\n    else:\n        return f'{sign}{seconds}s'\n
    "},{"location":"reference/pytanis/utils/#pytanis.utils.rm_keys","title":"rm_keys(keys: Any | list[Any], dct: dict[Any, Any]) -> dict[Any, Any]","text":"

    Return a copy with keys removed from dictionary

    Source code in src/pytanis/utils.py
    def rm_keys(\n    keys: Any | list[Any],\n    dct: dict[Any, Any],\n) -> dict[Any, Any]:\n    \"\"\"Return a copy with keys removed from dictionary\"\"\"\n    if not isinstance(keys, list):\n        keys = [keys]\n    return {k: v for k, v in dct.items() if k not in keys}\n
    "},{"location":"reference/pytanis/utils/#pytanis.utils.throttle","title":"throttle(calls: int, seconds: int = 1) -> Callable[[Callable[..., RT]], Callable[..., RT]]","text":"

    Decorator for throttling a function to number of calls per seconds

    Parameters:

    Name Type Description Default calls int

    number of calls per interval

    required seconds int

    number of seconds in interval

    1

    Returns:

    Type Description Callable[[Callable[..., RT]], Callable[..., RT]]

    wrapped function

    Source code in src/pytanis/utils.py
    def throttle(calls: int, seconds: int = 1) -> Callable[[Callable[..., RT]], Callable[..., RT]]:\n    \"\"\"Decorator for throttling a function to number of calls per seconds\n\n    Args:\n        calls: number of calls per interval\n        seconds: number of seconds in interval\n\n    Returns:\n        wrapped function\n    \"\"\"\n    if not isinstance(calls, int):\n        msg = 'number of calls must be integer'\n        raise ValueError(msg)\n    if not isinstance(seconds, int):\n        msg = 'number of seconds must be integer'\n        raise ValueError(msg)\n\n    def decorator(func: Callable[..., RT]) -> Callable[..., RT]:\n        # keeps track of the last calls\n        last_calls: list[float] = []\n        lock = threading.Lock()\n\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs) -> RT:\n            nonlocal last_calls\n            with lock:\n                curr_time = time.time()\n                # Remove old calls\n                last_calls = [call for call in last_calls if call > curr_time - seconds]\n\n                if len(last_calls) >= calls:\n                    sleep_time = last_calls[0] + seconds - curr_time\n                    logger = get_logger()\n                    logger.debug('stalling call', func=func.__name__, secs=sleep_time)\n                    time.sleep(sleep_time)\n\n                resp = func(*args, **kwargs)\n                last_calls.append(time.time())\n                return resp\n\n        return wrapper\n\n    return decorator\n
    "},{"location":"reference/pytanis/helpdesk/","title":"Helpdesk","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk","title":"helpdesk","text":"

    Functionality around the HelpDesk / LiveChat API

    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.__all__","title":"__all__ = ['HelpDeskClient', 'Mail', 'MailClient', 'Recipient'] module-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient","title":"HelpDeskClient(config: Config | None = None)","text":"Source code in src/pytanis/helpdesk/client.py
    def __init__(self, config: Config | None = None):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    # Important: Always use a custom User-Agent, never a generic one.\n    # Generic User-Agents are filtered by helpdesk to reduce spam.\n    self._headers = {'User-Agent': 'Pytanis'}\n\n    self._get_throttled = self._get\n    self._post_throttled = self._post\n    self.set_throttling(calls=1, seconds=2)  # Helpdesk is really strange when it comes to this\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.create_ticket","title":"create_ticket(ticket: NewTicket)","text":"Source code in src/pytanis/helpdesk/client.py
    def create_ticket(self, ticket: NewTicket):\n    return self.post('tickets', data=ticket.model_dump())\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.get","title":"get(endpoint: str, params: QueryParams | None = None) -> JSON","text":"

    Retrieve data via throttled GET request and return the JSON

    Source code in src/pytanis/helpdesk/client.py
    def get(self, endpoint: str, params: QueryParams | None = None) -> JSON:\n    \"\"\"Retrieve data via throttled GET request and return the JSON\"\"\"\n    resp = self._get_throttled(endpoint, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.list_agents","title":"list_agents() -> list[Agent]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_agents(self) -> list[Agent]:\n    agents = self.get('agents')\n    if not isinstance(agents, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Agent.model_validate(dct) for dct in agents]\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.list_teams","title":"list_teams() -> list[Team]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_teams(self) -> list[Team]:\n    teams = self.get('teams')\n    if not isinstance(teams, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Team.model_validate(dct) for dct in teams]\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.post","title":"post(endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON","text":"Source code in src/pytanis/helpdesk/client.py
    def post(self, endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON:\n    resp = self._post_throttled(endpoint, data, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/helpdesk/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.debug('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n    self._post_throttled = throttle(calls, seconds)(self._post)\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail","title":"Mail","text":"

    Mail template

    Use the data field to store additional information

    You can use the typical Format String Syntax and the objects recipient and mail to access metadata to complement the template, e.g.:

    Hello {recipient.address_as},\n\nWe hope it's ok to address you your first name rather than using your full name being {recipient.name}.\nHave you read the email's subject '{mail.subject}'? How is your work right now at {recipient.data.company}?\n\nCheers!\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.agent_id","title":"agent_id: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.data","title":"data: MetaData | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.recipients","title":"recipients: list[Recipient] instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.status","title":"status: str = 'solved' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.subject","title":"subject: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.team_id","title":"team_id: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.text","title":"text: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient","title":"MailClient(helpdesk_client: HelpDeskClient | None = None)","text":"

    Mail client for mass mails over HelpDesk

    Source code in src/pytanis/helpdesk/mail.py
    def __init__(self, helpdesk_client: HelpDeskClient | None = None):\n    if helpdesk_client is None:\n        helpdesk_client = HelpDeskClient()\n    self._helpdesk_client = helpdesk_client\n    self.dry_run: Callable[[NewTicket], None] = self.print_new_ticket\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient.batch_size","title":"batch_size: int = 20 class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient.dry_run","title":"dry_run: Callable[[NewTicket], None] = self.print_new_ticket instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient.wait_time","title":"wait_time: int = 30 class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient.print_new_ticket","title":"print_new_ticket(ticket: NewTicket) staticmethod","text":"

    Default action in a dry-run. Mainly for making sure you sent what you mean!

    Overwrite it by assigning to self.dry_run another function

    ToDo: Make this function nice, maybe use the rich library even

    Source code in src/pytanis/helpdesk/mail.py
    @staticmethod\ndef print_new_ticket(ticket: NewTicket):\n    \"\"\"Default action in a dry-run. Mainly for making sure you sent what you mean!\n\n    Overwrite it by assigning to self.dry_run another function\n\n    ToDo: Make this function nice, maybe use the `rich` library even\n    \"\"\"\n    print('#' * 40)  # noqa: T201\n    print(f'Recipient: {ticket.requester.name} <{ticket.requester.email}>')  # noqa: T201\n    print(f'Subject: {ticket.subject}')  # noqa: T201\n    print(f'{ticket.message.text}')  # noqa: T201\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient.send","title":"send(mail: Mail, *, dry_run: bool = True) -> tuple[list[tuple[Recipient, Ticket | None]], list[tuple[Recipient, Exception]]]","text":"

    Send a mail to all recipients using HelpDesk

    Source code in src/pytanis/helpdesk/mail.py
    def send(\n    self, mail: Mail, *, dry_run: bool = True\n) -> tuple[list[tuple[Recipient, Ticket | None]], list[tuple[Recipient, Exception]]]:\n    \"\"\"Send a mail to all recipients using HelpDesk\"\"\"\n    errors = []\n    tickets = []\n    for idx, recipient in enumerate(tqdm(mail.recipients), start=1):\n        recip_mail = mail.model_copy()\n        try:\n            recip_mail.subject = mail.subject.format(recipient=recipient, mail=mail)\n            # be aware here that the body might reference to subject line, so it must be filled already\n            recip_mail.text = recip_mail.text.format(recipient=recipient, mail=recip_mail)\n            ticket = self._create_ticket(recip_mail, recipient)\n            if dry_run:\n                self.print_new_ticket(ticket)\n                resp_ticket = None\n            else:\n                resp = self._helpdesk_client.create_ticket(ticket)\n                resp_ticket = Ticket.model_validate(resp)\n        except Exception as e:\n            errors.append((recipient, e))\n        else:\n            tickets.append((recipient, resp_ticket))\n        if idx % self.batch_size == 0:\n            time.sleep(self.wait_time)\n\n    return tickets, errors\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient","title":"Recipient","text":"

    Details about the recipient

    Use the data field to store additional information

    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient.address_as","title":"address_as: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient.data","title":"data: MetaData | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient.email","title":"email: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient.fill_with_name","title":"fill_with_name(v, values) classmethod","text":"Source code in src/pytanis/helpdesk/mail.py
    @validator('address_as')\n@classmethod\ndef fill_with_name(cls, v, values):\n    if v is None:\n        v = values['name']\n    return v\n
    "},{"location":"reference/pytanis/helpdesk/client/","title":"Client","text":""},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client","title":"client","text":"

    Client for the HelpDesk / LiveChat API

    Documentation: https://api.helpdesk.com/docs

    ToDo

    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.JSON","title":"JSON: TypeAlias = JSONObj | JSONLst module-attribute","text":"

    Type of the JSON response as returned by the HelpDesk / LiveChat API

    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.JSONLst","title":"JSONLst: TypeAlias = list[JSONObj] module-attribute","text":"

    Type of a JSON list of JSON objects

    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.JSONObj","title":"JSONObj: TypeAlias = dict[str, Any] module-attribute","text":"

    Type of a JSON object (without recursion)

    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient","title":"HelpDeskClient(config: Config | None = None)","text":"Source code in src/pytanis/helpdesk/client.py
    def __init__(self, config: Config | None = None):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    # Important: Always use a custom User-Agent, never a generic one.\n    # Generic User-Agents are filtered by helpdesk to reduce spam.\n    self._headers = {'User-Agent': 'Pytanis'}\n\n    self._get_throttled = self._get\n    self._post_throttled = self._post\n    self.set_throttling(calls=1, seconds=2)  # Helpdesk is really strange when it comes to this\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.create_ticket","title":"create_ticket(ticket: NewTicket)","text":"Source code in src/pytanis/helpdesk/client.py
    def create_ticket(self, ticket: NewTicket):\n    return self.post('tickets', data=ticket.model_dump())\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.get","title":"get(endpoint: str, params: QueryParams | None = None) -> JSON","text":"

    Retrieve data via throttled GET request and return the JSON

    Source code in src/pytanis/helpdesk/client.py
    def get(self, endpoint: str, params: QueryParams | None = None) -> JSON:\n    \"\"\"Retrieve data via throttled GET request and return the JSON\"\"\"\n    resp = self._get_throttled(endpoint, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.list_agents","title":"list_agents() -> list[Agent]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_agents(self) -> list[Agent]:\n    agents = self.get('agents')\n    if not isinstance(agents, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Agent.model_validate(dct) for dct in agents]\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.list_teams","title":"list_teams() -> list[Team]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_teams(self) -> list[Team]:\n    teams = self.get('teams')\n    if not isinstance(teams, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Team.model_validate(dct) for dct in teams]\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.post","title":"post(endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON","text":"Source code in src/pytanis/helpdesk/client.py
    def post(self, endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON:\n    resp = self._post_throttled(endpoint, data, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/helpdesk/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.debug('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n    self._post_throttled = throttle(calls, seconds)(self._post)\n
    "},{"location":"reference/pytanis/helpdesk/mail/","title":"Mail","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail","title":"mail","text":"

    Functionality around mailing

    ToDo

    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail","title":"Mail","text":"

    Mail template

    Use the data field to store additional information

    You can use the typical Format String Syntax and the objects recipient and mail to access metadata to complement the template, e.g.:

    Hello {recipient.address_as},\n\nWe hope it's ok to address you your first name rather than using your full name being {recipient.name}.\nHave you read the email's subject '{mail.subject}'? How is your work right now at {recipient.data.company}?\n\nCheers!\n
    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.agent_id","title":"agent_id: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.data","title":"data: MetaData | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.recipients","title":"recipients: list[Recipient] instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.status","title":"status: str = 'solved' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.subject","title":"subject: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.team_id","title":"team_id: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.text","title":"text: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient","title":"MailClient(helpdesk_client: HelpDeskClient | None = None)","text":"

    Mail client for mass mails over HelpDesk

    Source code in src/pytanis/helpdesk/mail.py
    def __init__(self, helpdesk_client: HelpDeskClient | None = None):\n    if helpdesk_client is None:\n        helpdesk_client = HelpDeskClient()\n    self._helpdesk_client = helpdesk_client\n    self.dry_run: Callable[[NewTicket], None] = self.print_new_ticket\n
    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient.batch_size","title":"batch_size: int = 20 class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient.dry_run","title":"dry_run: Callable[[NewTicket], None] = self.print_new_ticket instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient.wait_time","title":"wait_time: int = 30 class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient.print_new_ticket","title":"print_new_ticket(ticket: NewTicket) staticmethod","text":"

    Default action in a dry-run. Mainly for making sure you sent what you mean!

    Overwrite it by assigning to self.dry_run another function

    ToDo: Make this function nice, maybe use the rich library even

    Source code in src/pytanis/helpdesk/mail.py
    @staticmethod\ndef print_new_ticket(ticket: NewTicket):\n    \"\"\"Default action in a dry-run. Mainly for making sure you sent what you mean!\n\n    Overwrite it by assigning to self.dry_run another function\n\n    ToDo: Make this function nice, maybe use the `rich` library even\n    \"\"\"\n    print('#' * 40)  # noqa: T201\n    print(f'Recipient: {ticket.requester.name} <{ticket.requester.email}>')  # noqa: T201\n    print(f'Subject: {ticket.subject}')  # noqa: T201\n    print(f'{ticket.message.text}')  # noqa: T201\n
    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient.send","title":"send(mail: Mail, *, dry_run: bool = True) -> tuple[list[tuple[Recipient, Ticket | None]], list[tuple[Recipient, Exception]]]","text":"

    Send a mail to all recipients using HelpDesk

    Source code in src/pytanis/helpdesk/mail.py
    def send(\n    self, mail: Mail, *, dry_run: bool = True\n) -> tuple[list[tuple[Recipient, Ticket | None]], list[tuple[Recipient, Exception]]]:\n    \"\"\"Send a mail to all recipients using HelpDesk\"\"\"\n    errors = []\n    tickets = []\n    for idx, recipient in enumerate(tqdm(mail.recipients), start=1):\n        recip_mail = mail.model_copy()\n        try:\n            recip_mail.subject = mail.subject.format(recipient=recipient, mail=mail)\n            # be aware here that the body might reference to subject line, so it must be filled already\n            recip_mail.text = recip_mail.text.format(recipient=recipient, mail=recip_mail)\n            ticket = self._create_ticket(recip_mail, recipient)\n            if dry_run:\n                self.print_new_ticket(ticket)\n                resp_ticket = None\n            else:\n                resp = self._helpdesk_client.create_ticket(ticket)\n                resp_ticket = Ticket.model_validate(resp)\n        except Exception as e:\n            errors.append((recipient, e))\n        else:\n            tickets.append((recipient, resp_ticket))\n        if idx % self.batch_size == 0:\n            time.sleep(self.wait_time)\n\n    return tickets, errors\n
    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MetaData","title":"MetaData","text":"

    Additional, arbitrary metadata provided by the user like for template filling

    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient","title":"Recipient","text":"

    Details about the recipient

    Use the data field to store additional information

    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient.address_as","title":"address_as: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient.data","title":"data: MetaData | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient.email","title":"email: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient.fill_with_name","title":"fill_with_name(v, values) classmethod","text":"Source code in src/pytanis/helpdesk/mail.py
    @validator('address_as')\n@classmethod\ndef fill_with_name(cls, v, values):\n    if v is None:\n        v = values['name']\n    return v\n
    "},{"location":"reference/pytanis/helpdesk/types/","title":"Types","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types","title":"types","text":"

    Return types of the HelpDesk / LiveChat API

    Documentation: https://api.helpdesk.com/docs

    ToDo

    "},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Agent","title":"Agent","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Assignment","title":"Assignment","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Assignment.agent","title":"agent: Id instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Assignment.team","title":"team: Id instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Id","title":"Id","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Id.ID","title":"ID: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Message","title":"Message","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Message.text","title":"text: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket","title":"NewTicket","text":"

    Object that needs to be sent when creating a NEW ticket

    "},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.assignment","title":"assignment: Assignment | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.message","title":"message: Message instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.requester","title":"requester: Requester instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.status","title":"status: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.subject","title":"subject: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.teamIDs","title":"teamIDs: list[str] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Requester","title":"Requester","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Requester.email","title":"email: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Requester.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Team","title":"Team","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Ticket","title":"Ticket","text":"

    Actual ticket as returned by the API

    "},{"location":"reference/pytanis/pretalx/","title":"Pretalx","text":""},{"location":"reference/pytanis/pretalx/#pytanis.pretalx","title":"pretalx","text":"

    Functionality around the Pretalx API

    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.__all__","title":"__all__ = ['PretalxClient', 'subs_as_df', 'speakers_as_df', 'reviews_as_df'] module-attribute","text":""},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient","title":"PretalxClient(config: Config | None = None, *, blocking: bool = False)","text":"

    Client for the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def __init__(self, config: Config | None = None, *, blocking: bool = False):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    self._get_throttled = self._get\n    self.blocking = blocking\n    self.set_throttling(calls=2, seconds=1)  # we are nice by default and Pretalx doesn't allow many calls at once.\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.blocking","title":"blocking = blocking instance-attribute","text":""},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.answer","title":"answer(event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer","text":"

    Returns a specific answer

    Source code in src/pytanis/pretalx/client.py
    def answer(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer:  # noqa: A002\n    \"\"\"Returns a specific answer\"\"\"\n    return self._endpoint_id(Answer, event_slug, 'answers', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.answers","title":"answers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]","text":"

    Lists all answers and their details

    Source code in src/pytanis/pretalx/client.py
    def answers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]:\n    \"\"\"Lists all answers and their details\"\"\"\n    return self._endpoint_lst(Answer, event_slug, 'answers', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.event","title":"event(event_slug: str, *, params: QueryParams | None = None) -> Event","text":"

    Returns detailed information about a specific event

    Source code in src/pytanis/pretalx/client.py
    def event(self, event_slug: str, *, params: QueryParams | None = None) -> Event:\n    \"\"\"Returns detailed information about a specific event\"\"\"\n    endpoint = f'/api/events/{event_slug}/'\n    result = self._get_one(endpoint, params)\n    _logger.debug('result', resp=result)\n    return Event.model_validate(result)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.events","title":"events(*, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]","text":"

    Lists all events and their details

    Source code in src/pytanis/pretalx/client.py
    def events(self, *, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]:\n    \"\"\"Lists all events and their details\"\"\"\n    count, results = self._get_many('/api/events/', params)\n    events = iter(_logger.debug('result', resp=r) or Event.model_validate(r) for r in results)\n    return count, events\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.me","title":"me() -> Me","text":"

    Returns what Pretalx knows about myself

    Source code in src/pytanis/pretalx/client.py
    def me(self) -> Me:\n    \"\"\"Returns what Pretalx knows about myself\"\"\"\n    result = self._get_one('/api/me')\n    return Me.model_validate(result)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.question","title":"question(event_slug: str, id: int, *, params: QueryParams | None = None) -> Question","text":"

    Returns a specific question

    Source code in src/pytanis/pretalx/client.py
    def question(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Question:  # noqa: A002\n    \"\"\"Returns a specific question\"\"\"\n    return self._endpoint_id(Question, event_slug, 'questions', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.questions","title":"questions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]","text":"

    Lists all questions and their details

    Source code in src/pytanis/pretalx/client.py
    def questions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]:\n    \"\"\"Lists all questions and their details\"\"\"\n    return self._endpoint_lst(Question, event_slug, 'questions', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.review","title":"review(event_slug: str, id: int, *, params: QueryParams | None = None) -> Review","text":"

    Returns a specific review

    Source code in src/pytanis/pretalx/client.py
    def review(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Review:  # noqa: A002\n    \"\"\"Returns a specific review\"\"\"\n    return self._endpoint_id(Review, event_slug, 'reviews', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.reviews","title":"reviews(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]","text":"

    Lists all reviews and their details

    Source code in src/pytanis/pretalx/client.py
    def reviews(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]:\n    \"\"\"Lists all reviews and their details\"\"\"\n    return self._endpoint_lst(Review, event_slug, 'reviews', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.room","title":"room(event_slug: str, id: int, *, params: QueryParams | None = None) -> Room","text":"

    Returns a specific room

    Source code in src/pytanis/pretalx/client.py
    def room(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Room:  # noqa: A002\n    \"\"\"Returns a specific room\"\"\"\n    return self._endpoint_id(Room, event_slug, 'rooms', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.rooms","title":"rooms(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]","text":"

    Lists all rooms and their details

    Source code in src/pytanis/pretalx/client.py
    def rooms(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]:\n    \"\"\"Lists all rooms and their details\"\"\"\n    return self._endpoint_lst(Room, event_slug, 'rooms', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.info('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.speaker","title":"speaker(event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker","text":"

    Returns a specific speaker

    Source code in src/pytanis/pretalx/client.py
    def speaker(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker:\n    \"\"\"Returns a specific speaker\"\"\"\n    return self._endpoint_id(Speaker, event_slug, 'speakers', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.speakers","title":"speakers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]","text":"

    Lists all speakers and their details

    Source code in src/pytanis/pretalx/client.py
    def speakers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]:\n    \"\"\"Lists all speakers and their details\"\"\"\n    return self._endpoint_lst(Speaker, event_slug, 'speakers', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.submission","title":"submission(event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission","text":"

    Returns a specific submission

    Source code in src/pytanis/pretalx/client.py
    def submission(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission:\n    \"\"\"Returns a specific submission\"\"\"\n    return self._endpoint_id(Submission, event_slug, 'submissions', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.submissions","title":"submissions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]","text":"

    Lists all submissions and their details

    Source code in src/pytanis/pretalx/client.py
    def submissions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]:\n    \"\"\"Lists all submissions and their details\"\"\"\n    return self._endpoint_lst(Submission, event_slug, 'submissions', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.tag","title":"tag(event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag","text":"

    Returns a specific tag

    Source code in src/pytanis/pretalx/client.py
    def tag(self, event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag:\n    \"\"\"Returns a specific tag\"\"\"\n    return self._endpoint_id(Tag, event_slug, 'tags', tag, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.tags","title":"tags(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]","text":"

    Lists all tags and their details

    Source code in src/pytanis/pretalx/client.py
    def tags(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]:\n    \"\"\"Lists all tags and their details\"\"\"\n    return self._endpoint_lst(Tag, event_slug, 'tags', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.talk","title":"talk(event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk","text":"

    Returns a specific talk

    Source code in src/pytanis/pretalx/client.py
    def talk(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk:\n    \"\"\"Returns a specific talk\"\"\"\n    return self._endpoint_id(Talk, event_slug, 'talks', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.talks","title":"talks(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]","text":"

    Lists all talks and their details

    Source code in src/pytanis/pretalx/client.py
    def talks(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]:\n    \"\"\"Lists all talks and their details\"\"\"\n    return self._endpoint_lst(Talk, event_slug, 'talks', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.reviews_as_df","title":"reviews_as_df(reviews: Iterable[Review]) -> pd.DataFrame","text":"

    Convert the reviews to a dataframe

    Source code in src/pytanis/pretalx/utils.py
    def reviews_as_df(reviews: Iterable[Review]) -> pd.DataFrame:\n    \"\"\"Convert the reviews to a dataframe\"\"\"\n    df = pd.DataFrame([review.model_dump() for review in reviews])\n    # make first letter of column upper-case in accordance with our convention\n    df.rename(columns={col: col.title() for col in df.columns}, inplace=True)\n    # user is the speaker name to use for joining\n    df.rename(columns={'User': Col.pretalx_user, 'Score': Col.review_score}, inplace=True)\n\n    return df\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.speakers_as_df","title":"speakers_as_df(speakers: Iterable[Speaker], *, with_questions: bool = False, question_prefix: str = 'Q: ') -> pd.DataFrame","text":"

    Convert speakers into a dataframe

    Make sure to have params={\"questions\": \"all\"} for the PretalxAPI if with_questions is True.

    Source code in src/pytanis/pretalx/utils.py
    def speakers_as_df(\n    speakers: Iterable[Speaker], *, with_questions: bool = False, question_prefix: str = 'Q: '\n) -> pd.DataFrame:\n    \"\"\"Convert speakers into a dataframe\n\n    Make sure to have `params={\"questions\": \"all\"}` for the PretalxAPI if `with_questions` is True.\n    \"\"\"\n    rows = []\n    for speaker in speakers:\n        row = {\n            Col.speaker_code: speaker.code,\n            Col.speaker_name: speaker.name,\n            Col.email: speaker.email,\n            Col.biography: speaker.biography,\n            Col.submission: speaker.submissions,\n        }\n        if with_questions and speaker.answers is not None:\n            for answer in speaker.answers:\n                # The API returns also questions that are 'per proposal/submission', we get these using the\n                # submission endpoint and don't want them here due to ambiguity if several submission were made.\n                if answer.person is not None:\n                    row[f'{question_prefix}{answer.question.question.en}'] = answer.answer\n        rows.append(row)\n    return pd.DataFrame(rows)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.subs_as_df","title":"subs_as_df(subs: Iterable[Submission], *, with_questions: bool = False, question_prefix: str = 'Q: ') -> pd.DataFrame","text":"

    Convert submissions into a dataframe

    Make sure to have params={\"questions\": \"all\"} for the PretalxAPI if with_questions is True.

    Source code in src/pytanis/pretalx/utils.py
    def subs_as_df(\n    subs: Iterable[Submission], *, with_questions: bool = False, question_prefix: str = 'Q: '\n) -> pd.DataFrame:\n    \"\"\"Convert submissions into a dataframe\n\n    Make sure to have `params={\"questions\": \"all\"}` for the PretalxAPI if `with_questions` is True.\n    \"\"\"\n    rows = []\n    for sub in subs:\n        row = {\n            Col.submission: sub.code,\n            Col.title: sub.title,\n            Col.track: sub.track.en if sub.track else None,\n            Col.speaker_code: [speaker.code for speaker in sub.speakers],\n            Col.speaker_name: [speaker.name for speaker in sub.speakers],\n            Col.duration: sub.duration,\n            Col.submission_type: sub.submission_type.en,\n            Col.submission_type_id: sub.submission_type_id,\n            Col.state: sub.state.value,\n            Col.pending_state: None if sub.pending_state is None else sub.pending_state.value,\n            Col.created: sub.created,\n        }\n        if with_questions and sub.answers is not None:\n            for answer in sub.answers:\n                row[f'{question_prefix}{answer.question.question.en}'] = answer.answer\n        rows.append(row)\n    return pd.DataFrame(rows)\n
    "},{"location":"reference/pytanis/pretalx/client/","title":"Client","text":""},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client","title":"client","text":"

    Client for the Pretalx API

    Documentation: https://docs.pretalx.org/api/resources/index.html

    ToDo

    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.JSON","title":"JSON: TypeAlias = JSONObj | JSONLst module-attribute","text":"

    Type of the JSON response as returned by the Pretalx API

    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.JSONLst","title":"JSONLst: TypeAlias = list[JSONObj] module-attribute","text":"

    Type of a JSON list of JSON objects

    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.JSONObj","title":"JSONObj: TypeAlias = dict[str, Any] module-attribute","text":"

    Type of a JSON object (without recursion)

    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.T","title":"T = TypeVar('T', bound=BaseModel) module-attribute","text":""},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient","title":"PretalxClient(config: Config | None = None, *, blocking: bool = False)","text":"

    Client for the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def __init__(self, config: Config | None = None, *, blocking: bool = False):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    self._get_throttled = self._get\n    self.blocking = blocking\n    self.set_throttling(calls=2, seconds=1)  # we are nice by default and Pretalx doesn't allow many calls at once.\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.blocking","title":"blocking = blocking instance-attribute","text":""},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.answer","title":"answer(event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer","text":"

    Returns a specific answer

    Source code in src/pytanis/pretalx/client.py
    def answer(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer:  # noqa: A002\n    \"\"\"Returns a specific answer\"\"\"\n    return self._endpoint_id(Answer, event_slug, 'answers', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.answers","title":"answers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]","text":"

    Lists all answers and their details

    Source code in src/pytanis/pretalx/client.py
    def answers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]:\n    \"\"\"Lists all answers and their details\"\"\"\n    return self._endpoint_lst(Answer, event_slug, 'answers', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.event","title":"event(event_slug: str, *, params: QueryParams | None = None) -> Event","text":"

    Returns detailed information about a specific event

    Source code in src/pytanis/pretalx/client.py
    def event(self, event_slug: str, *, params: QueryParams | None = None) -> Event:\n    \"\"\"Returns detailed information about a specific event\"\"\"\n    endpoint = f'/api/events/{event_slug}/'\n    result = self._get_one(endpoint, params)\n    _logger.debug('result', resp=result)\n    return Event.model_validate(result)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.events","title":"events(*, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]","text":"

    Lists all events and their details

    Source code in src/pytanis/pretalx/client.py
    def events(self, *, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]:\n    \"\"\"Lists all events and their details\"\"\"\n    count, results = self._get_many('/api/events/', params)\n    events = iter(_logger.debug('result', resp=r) or Event.model_validate(r) for r in results)\n    return count, events\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.me","title":"me() -> Me","text":"

    Returns what Pretalx knows about myself

    Source code in src/pytanis/pretalx/client.py
    def me(self) -> Me:\n    \"\"\"Returns what Pretalx knows about myself\"\"\"\n    result = self._get_one('/api/me')\n    return Me.model_validate(result)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.question","title":"question(event_slug: str, id: int, *, params: QueryParams | None = None) -> Question","text":"

    Returns a specific question

    Source code in src/pytanis/pretalx/client.py
    def question(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Question:  # noqa: A002\n    \"\"\"Returns a specific question\"\"\"\n    return self._endpoint_id(Question, event_slug, 'questions', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.questions","title":"questions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]","text":"

    Lists all questions and their details

    Source code in src/pytanis/pretalx/client.py
    def questions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]:\n    \"\"\"Lists all questions and their details\"\"\"\n    return self._endpoint_lst(Question, event_slug, 'questions', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.review","title":"review(event_slug: str, id: int, *, params: QueryParams | None = None) -> Review","text":"

    Returns a specific review

    Source code in src/pytanis/pretalx/client.py
    def review(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Review:  # noqa: A002\n    \"\"\"Returns a specific review\"\"\"\n    return self._endpoint_id(Review, event_slug, 'reviews', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.reviews","title":"reviews(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]","text":"

    Lists all reviews and their details

    Source code in src/pytanis/pretalx/client.py
    def reviews(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]:\n    \"\"\"Lists all reviews and their details\"\"\"\n    return self._endpoint_lst(Review, event_slug, 'reviews', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.room","title":"room(event_slug: str, id: int, *, params: QueryParams | None = None) -> Room","text":"

    Returns a specific room

    Source code in src/pytanis/pretalx/client.py
    def room(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Room:  # noqa: A002\n    \"\"\"Returns a specific room\"\"\"\n    return self._endpoint_id(Room, event_slug, 'rooms', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.rooms","title":"rooms(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]","text":"

    Lists all rooms and their details

    Source code in src/pytanis/pretalx/client.py
    def rooms(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]:\n    \"\"\"Lists all rooms and their details\"\"\"\n    return self._endpoint_lst(Room, event_slug, 'rooms', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.info('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.speaker","title":"speaker(event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker","text":"

    Returns a specific speaker

    Source code in src/pytanis/pretalx/client.py
    def speaker(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker:\n    \"\"\"Returns a specific speaker\"\"\"\n    return self._endpoint_id(Speaker, event_slug, 'speakers', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.speakers","title":"speakers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]","text":"

    Lists all speakers and their details

    Source code in src/pytanis/pretalx/client.py
    def speakers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]:\n    \"\"\"Lists all speakers and their details\"\"\"\n    return self._endpoint_lst(Speaker, event_slug, 'speakers', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.submission","title":"submission(event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission","text":"

    Returns a specific submission

    Source code in src/pytanis/pretalx/client.py
    def submission(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission:\n    \"\"\"Returns a specific submission\"\"\"\n    return self._endpoint_id(Submission, event_slug, 'submissions', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.submissions","title":"submissions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]","text":"

    Lists all submissions and their details

    Source code in src/pytanis/pretalx/client.py
    def submissions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]:\n    \"\"\"Lists all submissions and their details\"\"\"\n    return self._endpoint_lst(Submission, event_slug, 'submissions', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.tag","title":"tag(event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag","text":"

    Returns a specific tag

    Source code in src/pytanis/pretalx/client.py
    def tag(self, event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag:\n    \"\"\"Returns a specific tag\"\"\"\n    return self._endpoint_id(Tag, event_slug, 'tags', tag, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.tags","title":"tags(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]","text":"

    Lists all tags and their details

    Source code in src/pytanis/pretalx/client.py
    def tags(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]:\n    \"\"\"Lists all tags and their details\"\"\"\n    return self._endpoint_lst(Tag, event_slug, 'tags', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.talk","title":"talk(event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk","text":"

    Returns a specific talk

    Source code in src/pytanis/pretalx/client.py
    def talk(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk:\n    \"\"\"Returns a specific talk\"\"\"\n    return self._endpoint_id(Talk, event_slug, 'talks', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.talks","title":"talks(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]","text":"

    Lists all talks and their details

    Source code in src/pytanis/pretalx/client.py
    def talks(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]:\n    \"\"\"Lists all talks and their details\"\"\"\n    return self._endpoint_lst(Talk, event_slug, 'talks', params=params)\n
    "},{"location":"reference/pytanis/pretalx/types/","title":"Types","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types","title":"types","text":"

    Return types of the Pretalx API

    Documentation: https://docs.pretalx.org/api/resources/index.html

    Attention: Quite often the API docs and the actual results of the API differ!

    ToDo

    "},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer","title":"Answer","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.answer","title":"answer: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.answer_file","title":"answer_file: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.options","title":"options: list[Option] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.person","title":"person: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.question","title":"question: AnswerQuestionRef instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.review","title":"review: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.submission","title":"submission: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.AnswerQuestionRef","title":"AnswerQuestionRef","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.AnswerQuestionRef.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.AnswerQuestionRef.question","title":"question: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event","title":"Event","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.date_from","title":"date_from: date instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.date_to","title":"date_to: date | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.is_public","title":"is_public: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.name","title":"name: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.slug","title":"slug: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.timezone","title":"timezone: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.urls","title":"urls: URLs instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Me","title":"Me","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Me.email","title":"email: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Me.local","title":"local: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Me.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Me.timezone","title":"timezone: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.MultiLingualStr","title":"MultiLingualStr","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.MultiLingualStr.de","title":"de: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.MultiLingualStr.en","title":"en: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Option","title":"Option","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Option.answer","title":"answer: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Option.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question","title":"Question","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.contains_personal_data","title":"contains_personal_data: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.deadline","title":"deadline: datetime | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.default_answer","title":"default_answer: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.freeze_after","title":"freeze_after: datetime | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.help_text","title":"help_text: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.is_public","title":"is_public: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.is_visible_to_reviewers","title":"is_visible_to_reviewers: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.max_length","title":"max_length: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.min_length","title":"min_length: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.options","title":"options: list[Option] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.question","title":"question: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.question_required","title":"question_required: QuestionRequirement instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.read_only","title":"read_only: bool | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.required","title":"required: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.target","title":"target: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.variant","title":"variant: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.QuestionRequirement","title":"QuestionRequirement","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.QuestionRequirement.after_deadline","title":"after_deadline = 'after deadline' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.QuestionRequirement.optional","title":"optional = 'optional' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.QuestionRequirement.required","title":"required = 'required' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Resource","title":"Resource","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Resource.description","title":"description: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Resource.resource","title":"resource: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review","title":"Review","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.answers","title":"answers: list[str] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.created","title":"created: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.score","title":"score: float | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.submission","title":"submission: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.text","title":"text: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.updated","title":"updated: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.user","title":"user: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room","title":"Room","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.availabilities","title":"availabilities: list[RoomAvailability] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.capacity","title":"capacity: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.description","title":"description: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.name","title":"name: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.position","title":"position: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.speaker_info","title":"speaker_info: MultiLingualStr | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.RoomAvailability","title":"RoomAvailability","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.RoomAvailability.end","title":"end: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.RoomAvailability.start","title":"start: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Slot","title":"Slot","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Slot.end","title":"end: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Slot.room","title":"room: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Slot.room_id","title":"room_id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Slot.start","title":"start: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Speaker","title":"Speaker","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Speaker.answers","title":"answers: list[Answer] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Speaker.availabilities","title":"availabilities: list[SpeakerAvailability] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Speaker.submissions","title":"submissions: list[str] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SpeakerAvailability","title":"SpeakerAvailability","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SpeakerAvailability.allDay","title":"allDay: str = Field(..., alias='all_day') class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SpeakerAvailability.end","title":"end: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SpeakerAvailability.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SpeakerAvailability.start","title":"start: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State","title":"State","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.accepted","title":"accepted = 'accepted' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.canceled","title":"canceled = 'canceled' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.confirmed","title":"confirmed = 'confirmed' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.deleted","title":"deleted = 'deleted' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.rejected","title":"rejected = 'rejected' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.submitted","title":"submitted = 'submitted' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.withdrawn","title":"withdrawn = 'withdrawn' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission","title":"Submission","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.abstract","title":"abstract: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.answers","title":"answers: list[Answer] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.code","title":"code: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.content_locale","title":"content_locale: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.created","title":"created: datetime | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.description","title":"description: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.do_not_record","title":"do_not_record: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.duration","title":"duration: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.image","title":"image: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.internal_notes","title":"internal_notes: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.is_featured","title":"is_featured: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.notes","title":"notes: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.pending_state","title":"pending_state: State | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.resources","title":"resources: list[Resource] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.slot","title":"slot: Slot | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.slot_count","title":"slot_count: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.speakers","title":"speakers: list[SubmissionSpeaker] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.state","title":"state: State instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.submission_type","title":"submission_type: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.submission_type_id","title":"submission_type_id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.tag_ids","title":"tag_ids: list[int] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.tags","title":"tags: list[str] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.title","title":"title: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.track","title":"track: MultiLingualStr | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.track_id","title":"track_id: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker","title":"SubmissionSpeaker","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker.avatar","title":"avatar: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker.biography","title":"biography: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker.code","title":"code: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker.email","title":"email: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Tag","title":"Tag","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Tag.color","title":"color: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Tag.description","title":"description: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Tag.tag","title":"tag: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Talk","title":"Talk","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.URLs","title":"URLs","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.URLs.base","title":"base: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.URLs.feed","title":"feed: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.URLs.login","title":"login: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.URLs.schedule","title":"schedule: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.User","title":"User","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.User.email","title":"email: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.User.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/","title":"Utils","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils","title":"utils","text":"

    Utilities related to Pretalx

    "},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col","title":"Col","text":"

    Convention of Pretalx column names for the functions below.

    "},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.affiliation","title":"affiliation = 'Affiliation' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.availability","title":"availability = 'Availability' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.availability_comment","title":"availability_comment = 'Availability Comment' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.biography","title":"biography = 'Biography' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.comment","title":"comment = 'Comment' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.created","title":"created = 'Created' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.duration","title":"duration = 'Duration' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.email","title":"email = 'Email' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.nreviews","title":"nreviews = '#Reviews' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.pending_state","title":"pending_state = 'Pending state' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.pretalx_user","title":"pretalx_user = 'Pretalx user' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.public","title":"public = 'Public' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.review_score","title":"review_score = 'Review Score' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.speaker_code","title":"speaker_code = 'Speaker code' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.speaker_name","title":"speaker_name = 'Speaker name' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.state","title":"state = 'State' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.submission","title":"submission = 'Submission' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.submission_type","title":"submission_type = 'Submission type' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.submission_type_id","title":"submission_type_id = 'Submission type id' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.title","title":"title = 'Title' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.track","title":"track = 'Track' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.reviews_as_df","title":"reviews_as_df(reviews: Iterable[Review]) -> pd.DataFrame","text":"

    Convert the reviews to a dataframe

    Source code in src/pytanis/pretalx/utils.py
    def reviews_as_df(reviews: Iterable[Review]) -> pd.DataFrame:\n    \"\"\"Convert the reviews to a dataframe\"\"\"\n    df = pd.DataFrame([review.model_dump() for review in reviews])\n    # make first letter of column upper-case in accordance with our convention\n    df.rename(columns={col: col.title() for col in df.columns}, inplace=True)\n    # user is the speaker name to use for joining\n    df.rename(columns={'User': Col.pretalx_user, 'Score': Col.review_score}, inplace=True)\n\n    return df\n
    "},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.speakers_as_df","title":"speakers_as_df(speakers: Iterable[Speaker], *, with_questions: bool = False, question_prefix: str = 'Q: ') -> pd.DataFrame","text":"

    Convert speakers into a dataframe

    Make sure to have params={\"questions\": \"all\"} for the PretalxAPI if with_questions is True.

    Source code in src/pytanis/pretalx/utils.py
    def speakers_as_df(\n    speakers: Iterable[Speaker], *, with_questions: bool = False, question_prefix: str = 'Q: '\n) -> pd.DataFrame:\n    \"\"\"Convert speakers into a dataframe\n\n    Make sure to have `params={\"questions\": \"all\"}` for the PretalxAPI if `with_questions` is True.\n    \"\"\"\n    rows = []\n    for speaker in speakers:\n        row = {\n            Col.speaker_code: speaker.code,\n            Col.speaker_name: speaker.name,\n            Col.email: speaker.email,\n            Col.biography: speaker.biography,\n            Col.submission: speaker.submissions,\n        }\n        if with_questions and speaker.answers is not None:\n            for answer in speaker.answers:\n                # The API returns also questions that are 'per proposal/submission', we get these using the\n                # submission endpoint and don't want them here due to ambiguity if several submission were made.\n                if answer.person is not None:\n                    row[f'{question_prefix}{answer.question.question.en}'] = answer.answer\n        rows.append(row)\n    return pd.DataFrame(rows)\n
    "},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.subs_as_df","title":"subs_as_df(subs: Iterable[Submission], *, with_questions: bool = False, question_prefix: str = 'Q: ') -> pd.DataFrame","text":"

    Convert submissions into a dataframe

    Make sure to have params={\"questions\": \"all\"} for the PretalxAPI if with_questions is True.

    Source code in src/pytanis/pretalx/utils.py
    def subs_as_df(\n    subs: Iterable[Submission], *, with_questions: bool = False, question_prefix: str = 'Q: '\n) -> pd.DataFrame:\n    \"\"\"Convert submissions into a dataframe\n\n    Make sure to have `params={\"questions\": \"all\"}` for the PretalxAPI if `with_questions` is True.\n    \"\"\"\n    rows = []\n    for sub in subs:\n        row = {\n            Col.submission: sub.code,\n            Col.title: sub.title,\n            Col.track: sub.track.en if sub.track else None,\n            Col.speaker_code: [speaker.code for speaker in sub.speakers],\n            Col.speaker_name: [speaker.name for speaker in sub.speakers],\n            Col.duration: sub.duration,\n            Col.submission_type: sub.submission_type.en,\n            Col.submission_type_id: sub.submission_type_id,\n            Col.state: sub.state.value,\n            Col.pending_state: None if sub.pending_state is None else sub.pending_state.value,\n            Col.created: sub.created,\n        }\n        if with_questions and sub.answers is not None:\n            for answer in sub.answers:\n                row[f'{question_prefix}{answer.question.question.en}'] = answer.answer\n        rows.append(row)\n    return pd.DataFrame(rows)\n
    "},{"location":"tasks/cfp/","title":"The Call for Participation Process","text":""},{"location":"tasks/cfp/#overview","title":"Overview","text":"

    The organisation of your conference programme starts with the Call for Participation/Proposals (CfP). Before you announce your CfP, obvious things to consider are:

    An example CfP can be found on the Call for Proposals of the PyConDE / PyData 2023 Berlin. The actual CfP submission is handled easily by Pretalx as you can just fill out everything in the web UI. As these tasks are all purely organisational/conceptional, there is no need to use Pytanis for any of this.

    "},{"location":"tasks/cfp/#example-milestones-of-pyconde-pydata-2023-berlin","title":"Example: Milestones of PyConDE / PyData 2023 Berlin","text":"

    Warning

    Obviously, you should never announce a CfP extension before the deadline of the original CfP is over \ud83d\ude1c. It is also useful to keep some of the above deadlines internally to avoid receiving a lot of emails when you have missed the deadline \ud83d\ude48.

    "},{"location":"tasks/review/","title":"The Review Process","text":""},{"location":"tasks/review/#overview","title":"Overview","text":"

    On a high-level, the review process of the proposals for a conference works as follows:

    1. find external reviewers and learn about their preferences,
    2. onboard reviewers in Pretalx,
    3. assign proposals to reviewers according to their preferences,
    4. communicate with the reviewers occasionally for updates,
    5. track the whole process.
    "},{"location":"tasks/review/#1-find-external-reviewers-and-learn-about-their-preferences","title":"1. Find External Reviewers and Learn about their Preferences","text":"

    For the PyConDE / PyData Berlin 2023, we were looking for about 50 external reviewers since we expected about 400 proposals, and we wanted to have 3 reviews per proposal. This would amount to about 25 proposals to review per person, which is manageable within a few weeks if you schedule 5-15 minutes per proposal.

    To get external reviewers, we decided that would only ask within our (Program Committee members') circle of trust and refer them to Google Forms. The form basically consisted of following questions with descriptions:

    Tip

    Every submission of the Google Form is then automatically added to a Google Sheet, let's call it the volunteer sheet, which can be easily read with the help of Pytanis. Check out our Google Sheet docs and Pytanis' google module to learn about more functionality.

    "},{"location":"tasks/review/#2-onboard-reviewers-in-pretalx","title":"2. Onboard Reviewers in Pretalx","text":"

    In Pretalx select Organisers in the left menu bar (you need Admin-rights for that) and click the teams under your event name. You should see a list of all teams and it's a good idea to have one for all reviewers, e.g. 2023-Reviewers-ALL. By clicking on the team name you get to a page that lists the names and corresponding e-mails of team members as well as an option to add new members at the bottom.

    You can now start typing in the e-mail addresses from the volunteer sheet to send out invitations to them. After volunteers accept the invitation they will show up with a user-name and e-mail in the team table. Now, here comes the tricky part that can cause a lot of confusion. If person A entered in the Google Form the e-mail address work@mail.com, and you added this in Pretalx, it might happen that person A accepts the team invitation with a different Pretalx account that is linked to the e-mail address private@mail.com. In this case, Pretalx will automatically replace work@mail.com, which was used for the invitation, with private@mail.com in the Pretalx table of team members. Unfortunately, Pretalx has no way of automatically tracking this change of mail addresses and this issue, as filed in #1417, is still unresolved.

    To work around this email issue and to be able to later join your volunteer sheet for instance with reviews, it makes sense to introduce a new column, e.g. \"Pretalx mail\", where you add the actual Pretalx account e-mail that was used by the invited user. Additionally, you should have a column for the Pretalx user-name, e.g. \"Pretalx user\", where you state the user-name by copying it over from the Pretalx team member table. This user-name column will be useful later to join our volunteer reviewers with the reviews they did, because the review-endpoint of Pretalx only returns the user-name, not the e-mail of a reviewer. This problem was also discussed in #1416 and is an intended behaviour.

    "},{"location":"tasks/review/#3-assign-proposals-to-reviewers-according-to-their-preferences","title":"3. Assign Proposals to Reviewers according to their Preferences","text":"

    Pretalx already provides a basic assignment feature so that proposals with the least number of reviews will show up earlier in the review queue so that they get more reviews. Additionally, Pretalx allows uploading a mapping JSON file so that you can assign certain proposals to a reviewer matching their preferences with the tracks of the proposals. Also, Pretalx is working on more elaborate automatic assignment features and some discussion about it can be found in issue #1331.

    Pytanis allows you to create JSON mapping files that can be uploaded in Pretalx under Review \u00bb Assign reviews. Then click Actions (upper right) \u00bb Import assignments and select the option Assign proposals to reviewers, choose the JSON file and make sure to always set Replace current assignments to Yes. Overwriting the current assignments makes sure that the assignment state in Pretalx is always consistent with what you expect. Also, be sure to always back up your assignment files somewhere in case you need to roll back later on. To make this easy, just name your files assignments-YYYYMMDD_I.json, where YYYY is the current year, MM the month, DD the day in the month and I the version increment, e.g. 1 or 2, in case you need several assignments throughout the same day.

    So how do you create an assignment file using Pytanis? Currently, we have implemented in a notebook an initial simple algorithm that can be easily run. Fancier algorithms will come in the future and don't hesitate to contribute. The main idea of the algorithm is to set a goal of number of reviews for each proposal, e.g. 3 reviews, and a certain buffer, e.g. 1. This means every proposal is assigned to goal number of reviews + buffer - current review number in case the current review number is not already equal or greater than the goal number of reviews. Rerunning this assignment frequently helps to avoid overshooting as the buffer mainly addresses the fact that you will also have inactive reviewers or some that start on the last day before your review deadline. For each proposal and remaining review, the algorithm assigns the proposals to:

    Be aware that some of your reviewers might have also make proposal submissions. Thus, it might happen by chance that someone gets assigned his/her own proposal using this approach but luckily Pretalx takes care of that--if the same Pretalx account was used.

    This quite simple algorithm can be found in the notebook 10_reviewer-assignment_v1. It uses Pytanis to pull the submission/proposals as well as the current reviews from Pretalx and joins them to get an overview of the current state of reviews. Then Pytanis is used to get the Google sheet of reviewers and their preferences, which is also joined with the data from Pretalx. Then the aforementioned algorithm is run and the assignment JSON file written.

    "},{"location":"tasks/review/#4-communicate-with-the-reviewers-occasionally-for-updates","title":"4. Communicate with the Reviewers occasionally for Updates","text":"

    From time to time, you want to get in contact with your reviewers to remind them of some deadline or just to say thank you for their work. Pytanis has an easy interface to HelpDesk that can be used as an e-mail client. For some practical examples, just check out the notebook 20_mail_to_reviewers_v1, the docs about mailing, as well as the Pytanis' mail references.

    "},{"location":"tasks/review/#5-track-the-whole-process","title":"5. Track the whole process","text":"

    During the review process it very important to keep track of review activity to make sure your internal deadlines for the review process are met. For instance, there might be reviewers that are having difficulties but have not reached out yet. So finding inactivate reviewers after a certain period of time and sending a nice supportive e-mail helps a lot. Also, some reviewers might have finished their batch of work early but might be up for more, thus identifying and getting in contact with them, is always a good idea. Many of those analyses are really individual, and you can check our examples in the notebook 10_reviewer-assignment_v1.

    "},{"location":"tasks/schedule/","title":"Creating the Schedule","text":"

    After all you talks and tutorials are confirmed, the next major milestone is to create a schedule so that each talk gets a time and place to be presented. Pretalx allows you to create a schedule by dragging & dropping the talk blocks onto a schedule, where you can define the number of days and rooms. You can also specify breaks like lunch or coffee breaks and later on publish the schedule for everyone. So this feature is pretty need but for larger conferences with a lot of parallel sessions, i.e. many rooms, some help might be needed.

    Assuming that you had some blank schedule before that already defines the time slots with their lengths and when the breaks are, then surely following constraints must be satisfied:

    Besides those constraints you might want to optimize for several objectives:

    1. the preferences for day and time of the speakers are considered (if they provided some),
    2. the more popular a talk is (from the public voting data), the more capacity the assigned room should have,
    3. if many people are highly interested in seeing two talks (voting data), these talks should rather not be scheduled in parallel. Also, sponsored talks should never be in parallel to avoid cannibalization,
    4. talks should have same main track, e.g. PyData, if they are in the same session (block of talks in one room),
    5. talks should have same sub track, e.g. PyData: Data Handling, if they are in the same session.

    The easiest way of dealing with multi-objective optimization is to create one new main objective by weighting and summing all objectives. For the objectives outlined above, it surely makes sense to choose the weights so that the importance is 1 > 2 > 3 > 4 > 5.

    In the notebook 50_scheduling_v1, you can find an example that uses Mixed-Integer-Programming (MIP) to generate a preliminary schedule that can be used as a starting point before creating the schedule in Pretalx. Although the constraints and objective from above may look quite simple, MIPs are not only hard, they are even NP-hard ;-) The example in the notebook uses Pyomo to formulate the problem and transform it into a standardized form, so that the solver HiGHS can do its job. In the concrete example, even after 24h no perfect solution was found, but the good thing is that the gap between best found feasible solution and the maximum possible objective value, i.e. the gap, was relatively small.

    Again, to visualize a solution like this, you can push it easily with the help of Pytanis to Google Sheets, which is illustrated in the figure below.

    Tip

    If you want to also specify link previews, sometimes also called a social banners, then check out the notebook 40_talk_image_v1 on how Pytanis can help you to create them.

    "},{"location":"tasks/selection/","title":"The Selection Process","text":""},{"location":"tasks/selection/#overview","title":"Overview","text":"

    On a high-level, the selection process involves the following

    1. have an optional public voting for the proposals,
    2. decide on how many talks, tutorials in which length, track or skill level you want to have,
    3. get an overview of the proposals, the speakers, the reviewer scores, and optionally the vote scores,
    4. select in Pretalx which talks are accepted and which ones are not.
    "},{"location":"tasks/selection/#1-optional-public-voting","title":"1. Optional Public Voting","text":"

    The pretalx-public-voting plugin allows to vote for the proposals which is a nice signal if a talk is generally interesting to the audience or not, solely based on the title and abstract. If it is installed activate it in Pretalx under Settings \u00bb Public voting. After the end date of the voting has passed this is also the place where you can download the results as a csv file. Unfortunately, there is currently no API provided by Pretalx for this feature.

    "},{"location":"tasks/selection/#2-decision-on-number-of-talks-and-rules-for-acceptance","title":"2. Decision on Number of Talks and Rules for Acceptance","text":"

    Deciding on the rules of acceptance might be one of the hardest parts and no Software can support you with it. It is really important to do this early on since it will help with the actual selection process. In order to decide for instance for the number of talks/tutorials in various lengths, it's important to already have a blank schedule, i.e. just the time slots, at hand. Diversity is also an important topic, so one rule might be to over-represent the under-represented but by how much? And do you expect your audience to be rather advanced, even senior, and what does that mean for ratio of the various required skill levels of the talks? How about the tracks you defined? Are speakers allowed to give more than one talk? How to deal with talks that have been given before? It's best to decide on a few guidelines before you proceed with the next steps.

    "},{"location":"tasks/selection/#3-overview-of-the-proposals","title":"3. Overview of the proposals","text":"

    Getting an overview of all proposals, their features, their review score and optionally their public score, is crucial when it comes to make a selection. Luckily with the help of Pytanis this is really easy. You can pull all the data from Pretalx, join it with additional data like the voting scores and push it to a Google Sheet, where everyone can easily view it and add comments. Find a practical example on how Pytanis was used for the PyConDE / PyData 2023 in this notebook 30_selection_v1.

    "},{"location":"tasks/selection/#4-final-selection-in-pretalx","title":"4. Final Selection in Pretalx","text":"

    Selecting the talks/tutorials for your conference is an iterative process. Maybe there are some talks you definitely want to select and others so bad you surely want to reject. Then there might be some you want to preliminarily accept or reject. Fortunately, Pretalx allows all that and Pytanis can pull that information to mark the rows in your GSheet with a certain colour. Here is an example on how this might look like.

    This example is also part of the notebook 30_selection_v1. Also be aware that after you accepted a talk or tutorial the author(s) must confirm. In practice, it happens also that accepted talks are withdrawn, so make sure you always keep a buffer of talks that haven't gotten any feedback yet to be able to accept some more.

    "},{"location":"usage/gsheet/","title":"Google Sheets","text":""},{"location":"usage/gsheet/#basic-usage","title":"Basic Usage","text":"

    Pytanis' Google Sheet client is really made for simplicity. Retrieving a worksheet of a Google sheet is as simple as:

    from pytanis import GSheetClient\n\ngsheet_client = GSheetClient()\ngsheet_df = gsheet_client.gsheet_as_df(SPREADSHEET_ID, WORKSHEET_NAME)\n
    where SPREADSHEET_ID is the ID taken from the spreadsheet's url, e.g. the ID is 17juVXM7V3p7Fgfi-9WkwPlMAYJB-DuxRhYCi_hastbB if your spreadsheet's url is https://docs.google.com/spreadsheets/d/17juVXM7V3p7Fgfi-9WkwPlMAYJB-DuxRhYCi_hastbB/edit#gid=1289752230, and WORKSHEET_NAME is the name of the actual sheet, e.g. Form responses 1, that you find in the lower bar of your spreadsheet. The function gsheet_as_df returns a simple Pandas dataframe, which most users are surely familiar with.

    If you run the above script the first time, you will get a link to a Google consent page, or it will directly open up if you run this in a Jupyter notebook. Read it carefully and accept the access to your Google Sheet. This step is only necessary and everytime you change the access scope. For instance, if you also want to have write-access to a worksheet, run:

    gsheet_client = GSheetClient(read_only=False)\ngsheet_client.recreate_token()\n
    and you will see the consent screen again, asking this time for write-access. Having accepted, you can now use
    gsheet_client.save_df_as_gsheet(subs_df, SPREADSHEET_ID, WORKSHEET_NAME)\n
    to upload a dataframe as Google sheet, overriding what's currently in there.

    Tip

    Google Sheet has a real useful version history that can be found under File \u00bb Version history \u00bb See version history. Even if you have accidentally overwritten you Google Sheet you can also restore an old version.

    "},{"location":"usage/gsheet/#advanced-usage","title":"Advanced Usage","text":"

    In case you want even more functionality and a dataframe is just not enough, you can use the gsheet method to get a Worksheet object or Spreadsheet object of GSpread. GSpread gives you full access to the API of Google Sheet and all the gsheet_as_df does is to basically use GSpread-Dataframe to convert this into a Pandas dataframe to simplify things for you. Also check out GSpread-Formatting if you want to use features like conditional formatting, colored cells, etc. Pytanis' google module gives you a complete reference of the current functionality within Pytanis but make sure to check out the GSpread ecosystem too as mentioned above.

    "},{"location":"usage/installation/","title":"Getting Started","text":""},{"location":"usage/installation/#installation","title":"Installation","text":"

    To install Pytanis simple run:

    pip install pytanis\n
    or to install all recommended additional dependencies:
    pip install 'pytanis[all]'\n
    Then create a configuration file and directory in your user's home directory. For Linux/MacOS/Unix use ~/.pytanis/config.toml and for Windows $HOME\\.pytanis\\config.toml, where $HOME is e.g. C:\\Users\\yourusername\\. Use your favourite editor to open config.toml within the .pytanis directory and add the following content:
    [Pretalx]\napi_token = \"932ndsf9uk32nf9sdkn3454532nj32jn\"\n\n[Google]\nclient_secret_json = \"client_secret.json\"\ntoken_json = \"token.json\"\n\n[HelpDesk]\naccount = \"934jcjkdf-39df-9df-93kf-934jfhuuij39fd\"\nentity_id = \"email@host.com\"\ntoken = \"dal:Sx4id934C3Y-X934jldjdfjk\"\n
    where you need to replace the dummy values in the sections [Pretalx] and [HelpDesk] accordingly.

    Info

    You have to configure the credentials and tokens only for the sections you actually want to use. For instance, [Pretalx] and [Google] are the most important sections for users that want to interact with Pretalx and also Google Sheets. If for instance no access to HelpDesk is necessary, e.g. no mails need to be sent, you can just leave out the key/value pairs in the [HelpDesk] section.

    "},{"location":"usage/installation/#retrieving-the-credentials-and-tokens","title":"Retrieving the Credentials and Tokens","text":""},{"location":"usage/mail/","title":"Sending Mails","text":""},{"location":"usage/mail/#basic-usage","title":"Basic Usage","text":"

    The usage of Pytanis' mail functionality is really simple. There are only three steps, you instantiate the mail client, create a mail object with your content and assemble a list of recipients.

    "},{"location":"usage/mail/#team-agent-id","title":"Team & Agent ID","text":"

    But before we write an e-mail we have to determine the team and agent id so that the e-mails we send are assigned to the right roles as set up within HelpDesk. In order to do this, we can just do:

    from pytanis import HelpDeskClient\n\nhelpdesk = HelpDeskClient()\n\nprint([agent.ID for agent in helpdesk.list_agents() if \"AGENTS EMAIL\" in agent.email])\nprint([team.ID for team in helpdesk.list_teams() if \"TEAM NAME\" in team.name])\n
    to find the right IDs with respect to the e-mail address AGENTS EMAIL and the corresponding TEAM NAME. We assume now that you stored those two values in agent_id and team_id, respectively.

    "},{"location":"usage/mail/#defining-the-recipients","title":"Defining the Recipients","text":"

    Defining the recipients means that you create a list of Recipient objects like:

    from pytanis.helpdesk import Recipient\n\nrecipients = [\n    Recipient(name=\"Peter Parker\", email=\"peter@parker.com\", address_as=\"Peter\"),\n    Recipient(name=\"Mary Watson\", email=\"marry-jane@watson.com\", address_as=\"Mary\"),\n]\n
    in most cases you will create this using a dataframe of some Google Sheet, and thus it will look more like:
    recipients = []\nrecip_df = google_sheet_df[[\"First name\", \"Last name\", \"E-mail\"]]\n\nfor _, row in recip_df.iterrows():\n    recipient = Recipient(\n        name=f\"{row['First name']} {row['Last name']}\",\n        email=row[\"E-mail\"],\n        address_as=row[\"First name\"],\n    )\n    recipients.append(recipient)\n

    For more advanced usages, e.g. individual mails corresponding to certain individuals, you can use the data parameter of the Recipient that takes a dictionary. Let's say we want to add a special sentence later for Peter to pay his rent, we can define:

    Recipient(\n    name=\"Peter Parker\",\n    email=\"peter@parker.com\",\n    address_as=\"Peter\",\n    data={\"feedback\": \"Pay your rent, Parker!\"},\n)\n
    In the section, we will see how we can access this special attribute again.

    "},{"location":"usage/mail/#writing-the-e-mail","title":"Writing the E-Mail","text":"

    So now we can write the actual e-mail text, which just uses the basic string substitution functionality of Python:

    mail_body = \"\"\"\nHi {recipient.address_as}!\n\nThis is a message from the Program committee with the subject {mail.subject} :-)\n{recipient.data.feedback}\n\nThank you very much {recipient.address_as} for your support!\n\nAll the best,\nProgram Committee\n\"\"\"\n
    You see that we can use recipient and mail to access the attributes of the Recipient as well as the Mail object to personalize the e-mail.

    Now we create the Mail object with:

    from pytanis.helpdesk import Mail\n\nmail = Mail(\n    subject=\"Deadline is coming soon\",\n    text=mail_body,\n    team_id=team_id,\n    agent_id=agent_id,\n    status=\"solved\",\n    recipients=recipients,\n)\n

    "},{"location":"usage/mail/#sending-an-e-mail","title":"Sending an E-mail","text":"

    Now we have everything assembled to send the e-mail with:

    from pytanis.helpdesk import MailClient\n\nmail_client = MailClient()\nresponses, errors = mail_client.send(mail, dry_run=True)\nassert not errors\n
    Having dry_run=True allows you to test you code and just print the resulting e-mails on your console to check if everything is like expected. Later set dry_run=False to actually send the e-mails via HelpDesk.

    The method send returns a list of successful responses and a hopefully empty list of errors. The responses list is a list of tuples where each tuple holds the Recipient as wells as the returned HelpDesk ticket. The errors list is a list of tuples with the Recipient and the corresponding exception object which occured when sending the mail to the recipient.

    "},{"location":"usage/mail/#advanced-usage","title":"Advanced Usage","text":"

    For more details, check out Pytanis' mail references and also the notebook 20_mail_to_reviewers_v1.

    Tip

    For contacting your (potential) speakers, Pretalx itself has pretty advanced templating and mailing features so there is no need to use this functionality here. Just make sure that you refer always to HelpDesk in your mails, so that you have a single point of managing mails and tickets.

    "},{"location":"usage/pretalx/","title":"Pretalx Client","text":""},{"location":"usage/pretalx/#basic-usage","title":"Basic Usage","text":"

    Pytanis offers easy access to the Pretalx API and the usage is quite self-explanatory. Let's look at some basic example:

    from pytanis import PretalxClient\n\nevent_name = \"pyconde-pydata-berlin-2023\"\n\npretalx_client = PretalxClient()\nsubs_count, subs = pretalx_client.submissions(event_name)\n
    This simple code will return the total number of submissions as subs_count and an iterator of all submissions subs. When iterating over subs new requests will be made internally to the Pretalx server to retrieve more result pages. This method of retrieving partial results is called pagination. Quite often you will just use subs = list(subs) to retrieve all submissions and get a list instead for easier handling. If you want to retrieve always all results directly, i.e. in a blocking way, you can tell this to the client via PretalxClient(blocking=True) but be aware that you must still call subs = list(subs).

    All endpoints of the Pretalx API are implemented in Pytanis and the method name corresponds to the name of the endpoint. Additional parameters can be passed using the params argument like e.g.:

    subs_count, subs = pretalx_client.submissions(\n    event_name, params={\"questions\": \"all\", \"state\": \"submitted\"}\n)\n
    Check the Pretalx API for a list of options.

    "},{"location":"usage/pretalx/#advanced-usage","title":"Advanced Usage","text":"

    Find out more about the client's capabilities, e.g. throttling, by looking at Pytanis' reference of the pretalx client module.

    "}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\u200b","text":"

    Pytanis includes a Pretalx client and all the tooling you need for conferences using Pretalx, from handling the initial call for papers to creating the final program.

    Trivia: The name Pytanis is a reference to Prytanis using the typical py prefix of Python tools. Prytanis was the name given to the leading members of the government of a city (polis) in ancient Greece. Offices that used this title usually had responsibility for presiding over councils of some kind, which met in the Prytaneion. Romani ite domum!

    "},{"location":"#features","title":"Features","text":""},{"location":"#license","title":"License","text":"

    Pytanis is distributed under the terms of the MIT license.

    "},{"location":"#navigation","title":"Navigation","text":"

    Documentation for specific MAJOR.MINOR versions can be chosen by using the dropdown on the top of every page. The dev version reflects changes that have not yet been released.

    Also, desktop readers can use special keyboard shortcuts:

    Keys Action Navigate to the \"previous\" page Navigate to the \"next\" page Display the search modal"},{"location":"authors/","title":"Authors","text":""},{"location":"authors/#contributors","title":"Contributors","text":""},{"location":"changelog/","title":"Changelog","text":""},{"location":"changelog/#changelog","title":"Changelog","text":""},{"location":"changelog/#version-07","title":"Version 0.7","text":""},{"location":"changelog/#version-061-2023-12-10","title":"Version 0.6.1 (2023-12-10)","text":""},{"location":"changelog/#version-06-2023-12-04","title":"Version 0.6 (2023-12-04)","text":""},{"location":"changelog/#version-05-2023-04-10","title":"Version 0.5 (2023-04-10)","text":""},{"location":"changelog/#version-041-2023-03-25","title":"Version 0.4.1 (2023-03-25)","text":""},{"location":"changelog/#version-04-2023-03-10","title":"Version 0.4 (2023-03-10)","text":""},{"location":"changelog/#version-03-2023-02-17","title":"Version 0.3 (2023-02-17)","text":""},{"location":"changelog/#version-02-2023-02-11","title":"Version 0.2 (2023-02-11)","text":""},{"location":"changelog/#version-011-2023-01-16","title":"Version 0.1.1 (2023-01-16)","text":""},{"location":"changelog/#version-01-2023-01-15","title":"Version 0.1 (2023-01-15)","text":""},{"location":"contributing/","title":"Contributing","text":""},{"location":"contributing/#contributing","title":"Contributing","text":"

    Welcome to the contributor guide of Pytanis.

    This document focuses on getting any potential contributor familiarized with the development processes, but other kinds of contributions are also appreciated.

    If you are new to using git or have never collaborated on a project previously, please have a look at contribution-guide.org. Other resources are also listed in the excellent guide created by Freecodecamp1.

    Please note: all users and contributors are expected to be open, considerate, reasonable, and respectful. When in doubt, Python Software Foundation's Code of Conduct is a good reference in terms of behavior guidelines.

    "},{"location":"contributing/#issue-reports","title":"Issue Reports","text":"

    If you experience bugs or general issues with Pytanis, please have a look at the issue tracker. If you don't see anything useful there, please feel free to file an issue report.

    Tip

    Please don't forget to include the closed issues in your search. Sometimes a solution will have been reported already and the problem is considered solved.

    New issue reports should include information about your programming environment (e.g., operating system, Python version) and steps to reproduce the problem. Please try also to simplify the reproduction steps to a very minimal example that still illustrates the problem you are facing. By removing other factors, you help us to identify the root cause of the issue.

    "},{"location":"contributing/#documentation-improvements","title":"Documentation improvements","text":"

    You can contribute to the documentation of Pytanis by making them more readable and coherent, or by adding missing information and correcting mistakes.

    The documentation uses mkdocs as its main documentation compiler. This means that the docs are kept in the same repository as the project code, and that any documentation update is done in the same way was a code contribution.

    Tip

    Please note that the GitHub web interface provides a quick way of propose changes in Pytanis' files. While this mechanism can be tricky for normal code contributions, it works perfectly fine for contributing to the docs, and can be quite handy.

    If you are interested in trying this method out, please navigate to the docs folder in the source repository, find which file you would like to propose changes and click in the little pencil icon at the top to open GitHub's code editor. Once you finish editing the file, please write a message in the form at the bottom of the page describing which changes have you made and what are the motivations behind them and submit your proposal.

    When working on documentation changes in your local machine, you can build and serve them using hatch with hatch run docs:build and hatch run docs:serve, respectively.

    "},{"location":"contributing/#code-contributions","title":"Code Contributions","text":""},{"location":"contributing/#submit-an-issue","title":"Submit an issue","text":"

    Before you work on any non-trivial code contribution it's best to first create a report in the issue tracker to start a discussion on the subject. This often provides additional considerations and avoids unnecessary work.

    "},{"location":"contributing/#clone-the-repository","title":"Clone the repository","text":"
    1. Create a user account on GitHub if you do not already have one.

    2. Fork the project repository: click on the Fork button near the top of the page. This creates a copy of the code under your account on GitHub.

    3. Clone this copy to your local disk:

      git clone git@github.com:YourLogin/pytanis.git\ncd pytanis\n

    4. Make sure hatch is installed using pipx:

      pipx install hatch\n

    5. [only once] install pre-commit hooks in the default environment with:

      hatch run pre-commit install\n

    "},{"location":"contributing/#implement-your-changes","title":"Implement your changes","text":"
    1. Create a branch to hold your changes:

      git checkout -b my-feature\n
      and start making changes. Never work on the main branch!

    2. Start your work on this branch. Don't forget to add docstrings in Google style to new functions, modules and classes, especially if they are part of public APIs.

    3. Add yourself to the list of contributors in AUTHORS.md.

    4. When you\u2019re done editing, do:

      git add <MODIFIED FILES>\ngit commit\n
      to record your changes in git. Please make sure to see the validation messages from pre-commit and fix any eventual issues. This should automatically use flake8/black to check/fix the code style in a way that is compatible with the project.

      Info

      Don't forget to add unit tests and documentation in case your contribution adds a feature and is not just a bugfix.

      Moreover, writing an descriptive commit message is highly recommended. In case of doubt, you can check the commit history with:

      git log --graph --decorate --pretty=oneline --abbrev-commit --all\n
      to look for recurring communication patterns.

    5. Please check that your changes don't break any unit tests with hatch run cov or hatch run no-cov to run the unitest with or without coverage reports, respectively.

    6. For code hygiene, execute hatch run lint:all in order to run flake8, isort, black, mypy, etc.
    "},{"location":"contributing/#submit-your-contribution","title":"Submit your contribution","text":"
    1. If everything works fine, push your local branch to the remote server with:
    git push -u origin my-feature\n
    1. Go to the web page of your fork and click \"Create pull request\" to send your changes for review.

    Find more detailed information in creating a PR. You might also want to open the PR as a draft first and mark it as ready for review after the feedbacks from the continuous integration (CI) system or any required fixes.

    1. Even though these resources focus on open source projects and communities, the general ideas behind collaborating with other developers to collectively create software are general and can be applied to all sorts of environments, including private companies and proprietary code bases.\u00a0\u21a9

    "},{"location":"license/","title":"License","text":"

    The MIT License (MIT)

    Copyright \u00a9 2023 Florian Wilhelm

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    "},{"location":"reference/SUMMARY/","title":"SUMMARY","text":""},{"location":"reference/pytanis/","title":"Reference","text":""},{"location":"reference/pytanis/#pytanis","title":"pytanis","text":""},{"location":"reference/pytanis/#pytanis.__all__","title":"__all__ = ['__version__', 'GSheetClient', 'PretalxClient', 'HelpDeskClient', 'get_cfg'] module-attribute","text":""},{"location":"reference/pytanis/#pytanis.__version__","title":"__version__ = version('pytanis') module-attribute","text":""},{"location":"reference/pytanis/#pytanis.GSheetClient","title":"GSheetClient(config: Config | None = None, *, read_only: bool = True)","text":"

    Google API to easily handle GSheets and other files on GDrive

    By default, only the least permissive scope GSHEET_RO in case of read_only = True is used.

    Source code in src/pytanis/google.py
    def __init__(self, config: Config | None = None, *, read_only: bool = True):\n    self._read_only = read_only\n    if read_only:\n        self._scopes = [Scope.GSHEET_RO]\n    else:\n        self._scopes = [Scope.GSHEET_RW]\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    self.gc = gspread_client(self._scopes, config)  # gspread client for more functionality\n
    "},{"location":"reference/pytanis/#pytanis.GSheetClient.gc","title":"gc = gspread_client(self._scopes, config) instance-attribute","text":""},{"location":"reference/pytanis/#pytanis.GSheetClient.clear_gsheet","title":"clear_gsheet(spreadsheet_id: str, worksheet_name: str)","text":"

    Clear the worksheet including values, formatting, filtering, etc.

    Source code in src/pytanis/google.py
    def clear_gsheet(self, spreadsheet_id: str, worksheet_name: str):\n    \"\"\"Clear the worksheet including values, formatting, filtering, etc.\"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name, create_ws=False)\n    default_fmt = get_default_format(worksheet.spreadsheet)\n    wrange = worksheet_range(worksheet)\n    try:\n        worksheet.clear()\n        worksheet.clear_basic_filter()\n        format_cell_range(worksheet, wrange, default_fmt)\n        rules = get_conditional_format_rules(worksheet)\n        rules.clear()\n        rules.save()\n        set_data_validation_for_cell_range(worksheet, wrange, None)\n    except APIError as error:\n        self._exception_feedback(error)\n
    "},{"location":"reference/pytanis/#pytanis.GSheetClient.gsheet","title":"gsheet(spreadsheet_id: str, worksheet_name: str | None = None, *, create_ws: bool = False) -> Worksheet | Spreadsheet","text":"

    Retrieve a Google sheet by its id and the name

    Open a Google sheet in your browser and check the URL to retrieve the id, e.g.: https://docs.google.com/spreadsheets/d/SPREEDSHEET_ID/edit...

    If the spreadsheet as several worksheets (check the lower bar) then worksheet_name can be used to specify a specific one.

    Source code in src/pytanis/google.py
    def gsheet(\n    self, spreadsheet_id: str, worksheet_name: str | None = None, *, create_ws: bool = False\n) -> Worksheet | Spreadsheet:\n    \"\"\"Retrieve a Google sheet by its id and the name\n\n    Open a Google sheet in your browser and check the URL to retrieve the id, e.g.:\n    https://docs.google.com/spreadsheets/d/SPREEDSHEET_ID/edit...\n\n    If the spreadsheet as several worksheets (check the lower bar) then `worksheet_name` can be used to\n    specify a specific one.\n    \"\"\"\n    spreadsheet = self.gc.open_by_key(spreadsheet_id)\n    if worksheet_name is None:\n        return spreadsheet\n    elif worksheet_name in [ws.title for ws in spreadsheet.worksheets()]:\n        return spreadsheet.worksheet(worksheet_name)\n    elif create_ws:\n        worksheet = spreadsheet.add_worksheet(title=worksheet_name, rows=100, cols=20)\n        self._wait_for_worksheet(spreadsheet_id, worksheet_name)\n        return worksheet\n    else:\n        return spreadsheet.worksheet(worksheet_name)  # raises exception\n
    "},{"location":"reference/pytanis/#pytanis.GSheetClient.gsheet_as_df","title":"gsheet_as_df(spreadsheet_id: str, worksheet_name: str, **kwargs: str | bool | int) -> pd.DataFrame","text":"

    Returns a worksheet as dataframe

    Source code in src/pytanis/google.py
    def gsheet_as_df(self, spreadsheet_id: str, worksheet_name: str, **kwargs: str | (bool | int)) -> pd.DataFrame:\n    \"\"\"Returns a worksheet as dataframe\"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name)\n    df = get_as_dataframe(worksheet, **kwargs)\n    # remove Nan rows & columns as they are exported by default\n    df.dropna(how='all', inplace=True, axis=0)\n    df.dropna(how='all', inplace=True, axis=1)\n    return df\n
    "},{"location":"reference/pytanis/#pytanis.GSheetClient.recreate_token","title":"recreate_token()","text":"

    Recreate the current token using the scopes given at initialization

    Source code in src/pytanis/google.py
    def recreate_token(self):\n    \"\"\"Recreate the current token using the scopes given at initialization\"\"\"\n    self._config.Google.token_json.unlink(missing_ok=True)\n    self.gc = gspread_client(self._scopes, self._config)\n
    "},{"location":"reference/pytanis/#pytanis.GSheetClient.save_df_as_gsheet","title":"save_df_as_gsheet(df: pd.DataFrame, spreadsheet_id: str, worksheet_name: str, *, create_ws: bool = False, default_fmt: bool = True, **kwargs: str | bool | int)","text":"

    Save the given dataframe as worksheet in a spreadsheet

    Make sure that the scope passed gives you write permissions

    Parameters:

    Name Type Description Default df DataFrame

    dataframe to save

    required spreadsheet_id str

    id of the Google spreadsheet

    required worksheet_name str

    name of the worksheet within the spreadsheet

    required create_ws bool

    create the worksheet if non-existent

    False default_fmt bool

    apply default formatter BasicFormatter

    True **kwargs str | bool | int

    extra keyword arguments passed to set_with_dataframe

    {} Source code in src/pytanis/google.py
    def save_df_as_gsheet(\n    self,\n    df: pd.DataFrame,\n    spreadsheet_id: str,\n    worksheet_name: str,\n    *,\n    create_ws: bool = False,\n    default_fmt: bool = True,\n    **kwargs: str | (bool | int),\n):\n    \"\"\"Save the given dataframe as worksheet in a spreadsheet\n\n    Make sure that the scope passed gives you write permissions\n\n    Args:\n        df: dataframe to save\n        spreadsheet_id: id of the Google spreadsheet\n        worksheet_name: name of the worksheet within the spreadsheet\n        create_ws: create the worksheet if non-existent\n        default_fmt: apply default formatter `BasicFormatter`\n        **kwargs: extra keyword arguments passed to `set_with_dataframe`\n    \"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name, create_ws=create_ws)\n    # make sure it's really only the dataframe, not some residue\n    self.clear_gsheet(spreadsheet_id, worksheet_name)\n    params = {'resize': True} | dict(**kwargs)  # set sane defaults\n    try:\n        set_with_dataframe(worksheet, df, **params)\n        if default_fmt:\n            format_with_dataframe(worksheet, df)\n    except APIError as error:\n        self._exception_feedback(error)\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient","title":"HelpDeskClient(config: Config | None = None)","text":"Source code in src/pytanis/helpdesk/client.py
    def __init__(self, config: Config | None = None):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    # Important: Always use a custom User-Agent, never a generic one.\n    # Generic User-Agents are filtered by helpdesk to reduce spam.\n    self._headers = {'User-Agent': 'Pytanis'}\n\n    self._get_throttled = self._get\n    self._post_throttled = self._post\n    self.set_throttling(calls=1, seconds=2)  # Helpdesk is really strange when it comes to this\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.create_ticket","title":"create_ticket(ticket: NewTicket)","text":"Source code in src/pytanis/helpdesk/client.py
    def create_ticket(self, ticket: NewTicket):\n    return self.post('tickets', data=ticket.model_dump())\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.get","title":"get(endpoint: str, params: QueryParams | None = None) -> JSON","text":"

    Retrieve data via throttled GET request and return the JSON

    Source code in src/pytanis/helpdesk/client.py
    def get(self, endpoint: str, params: QueryParams | None = None) -> JSON:\n    \"\"\"Retrieve data via throttled GET request and return the JSON\"\"\"\n    resp = self._get_throttled(endpoint, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.list_agents","title":"list_agents() -> list[Agent]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_agents(self) -> list[Agent]:\n    agents = self.get('agents')\n    if not isinstance(agents, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Agent.model_validate(dct) for dct in agents]\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.list_teams","title":"list_teams() -> list[Team]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_teams(self) -> list[Team]:\n    teams = self.get('teams')\n    if not isinstance(teams, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Team.model_validate(dct) for dct in teams]\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.post","title":"post(endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON","text":"Source code in src/pytanis/helpdesk/client.py
    def post(self, endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON:\n    resp = self._post_throttled(endpoint, data, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/#pytanis.HelpDeskClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/helpdesk/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.debug('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n    self._post_throttled = throttle(calls, seconds)(self._post)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient","title":"PretalxClient(config: Config | None = None, *, blocking: bool = False)","text":"

    Client for the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def __init__(self, config: Config | None = None, *, blocking: bool = False):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    self._get_throttled = self._get\n    self.blocking = blocking\n    self.set_throttling(calls=2, seconds=1)  # we are nice by default and Pretalx doesn't allow many calls at once.\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.blocking","title":"blocking = blocking instance-attribute","text":""},{"location":"reference/pytanis/#pytanis.PretalxClient.answer","title":"answer(event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer","text":"

    Returns a specific answer

    Source code in src/pytanis/pretalx/client.py
    def answer(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer:  # noqa: A002\n    \"\"\"Returns a specific answer\"\"\"\n    return self._endpoint_id(Answer, event_slug, 'answers', id, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.answers","title":"answers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]","text":"

    Lists all answers and their details

    Source code in src/pytanis/pretalx/client.py
    def answers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]:\n    \"\"\"Lists all answers and their details\"\"\"\n    return self._endpoint_lst(Answer, event_slug, 'answers', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.event","title":"event(event_slug: str, *, params: QueryParams | None = None) -> Event","text":"

    Returns detailed information about a specific event

    Source code in src/pytanis/pretalx/client.py
    def event(self, event_slug: str, *, params: QueryParams | None = None) -> Event:\n    \"\"\"Returns detailed information about a specific event\"\"\"\n    endpoint = f'/api/events/{event_slug}/'\n    result = self._get_one(endpoint, params)\n    _logger.debug('result', resp=result)\n    return Event.model_validate(result)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.events","title":"events(*, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]","text":"

    Lists all events and their details

    Source code in src/pytanis/pretalx/client.py
    def events(self, *, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]:\n    \"\"\"Lists all events and their details\"\"\"\n    count, results = self._get_many('/api/events/', params)\n    events = iter(_logger.debug('result', resp=r) or Event.model_validate(r) for r in results)\n    return count, events\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.me","title":"me() -> Me","text":"

    Returns what Pretalx knows about myself

    Source code in src/pytanis/pretalx/client.py
    def me(self) -> Me:\n    \"\"\"Returns what Pretalx knows about myself\"\"\"\n    result = self._get_one('/api/me')\n    return Me.model_validate(result)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.question","title":"question(event_slug: str, id: int, *, params: QueryParams | None = None) -> Question","text":"

    Returns a specific question

    Source code in src/pytanis/pretalx/client.py
    def question(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Question:  # noqa: A002\n    \"\"\"Returns a specific question\"\"\"\n    return self._endpoint_id(Question, event_slug, 'questions', id, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.questions","title":"questions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]","text":"

    Lists all questions and their details

    Source code in src/pytanis/pretalx/client.py
    def questions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]:\n    \"\"\"Lists all questions and their details\"\"\"\n    return self._endpoint_lst(Question, event_slug, 'questions', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.review","title":"review(event_slug: str, id: int, *, params: QueryParams | None = None) -> Review","text":"

    Returns a specific review

    Source code in src/pytanis/pretalx/client.py
    def review(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Review:  # noqa: A002\n    \"\"\"Returns a specific review\"\"\"\n    return self._endpoint_id(Review, event_slug, 'reviews', id, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.reviews","title":"reviews(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]","text":"

    Lists all reviews and their details

    Source code in src/pytanis/pretalx/client.py
    def reviews(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]:\n    \"\"\"Lists all reviews and their details\"\"\"\n    return self._endpoint_lst(Review, event_slug, 'reviews', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.room","title":"room(event_slug: str, id: int, *, params: QueryParams | None = None) -> Room","text":"

    Returns a specific room

    Source code in src/pytanis/pretalx/client.py
    def room(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Room:  # noqa: A002\n    \"\"\"Returns a specific room\"\"\"\n    return self._endpoint_id(Room, event_slug, 'rooms', id, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.rooms","title":"rooms(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]","text":"

    Lists all rooms and their details

    Source code in src/pytanis/pretalx/client.py
    def rooms(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]:\n    \"\"\"Lists all rooms and their details\"\"\"\n    return self._endpoint_lst(Room, event_slug, 'rooms', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.info('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.speaker","title":"speaker(event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker","text":"

    Returns a specific speaker

    Source code in src/pytanis/pretalx/client.py
    def speaker(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker:\n    \"\"\"Returns a specific speaker\"\"\"\n    return self._endpoint_id(Speaker, event_slug, 'speakers', code, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.speakers","title":"speakers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]","text":"

    Lists all speakers and their details

    Source code in src/pytanis/pretalx/client.py
    def speakers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]:\n    \"\"\"Lists all speakers and their details\"\"\"\n    return self._endpoint_lst(Speaker, event_slug, 'speakers', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.submission","title":"submission(event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission","text":"

    Returns a specific submission

    Source code in src/pytanis/pretalx/client.py
    def submission(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission:\n    \"\"\"Returns a specific submission\"\"\"\n    return self._endpoint_id(Submission, event_slug, 'submissions', code, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.submissions","title":"submissions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]","text":"

    Lists all submissions and their details

    Source code in src/pytanis/pretalx/client.py
    def submissions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]:\n    \"\"\"Lists all submissions and their details\"\"\"\n    return self._endpoint_lst(Submission, event_slug, 'submissions', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.tag","title":"tag(event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag","text":"

    Returns a specific tag

    Source code in src/pytanis/pretalx/client.py
    def tag(self, event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag:\n    \"\"\"Returns a specific tag\"\"\"\n    return self._endpoint_id(Tag, event_slug, 'tags', tag, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.tags","title":"tags(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]","text":"

    Lists all tags and their details

    Source code in src/pytanis/pretalx/client.py
    def tags(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]:\n    \"\"\"Lists all tags and their details\"\"\"\n    return self._endpoint_lst(Tag, event_slug, 'tags', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.talk","title":"talk(event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk","text":"

    Returns a specific talk

    Source code in src/pytanis/pretalx/client.py
    def talk(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk:\n    \"\"\"Returns a specific talk\"\"\"\n    return self._endpoint_id(Talk, event_slug, 'talks', code, params=params)\n
    "},{"location":"reference/pytanis/#pytanis.PretalxClient.talks","title":"talks(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]","text":"

    Lists all talks and their details

    Source code in src/pytanis/pretalx/client.py
    def talks(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]:\n    \"\"\"Lists all talks and their details\"\"\"\n    return self._endpoint_lst(Talk, event_slug, 'talks', params=params)\n
    "},{"location":"reference/pytanis/#pytanis.get_cfg","title":"get_cfg() -> Config","text":"

    Returns the configuration as an object

    Source code in src/pytanis/config.py
    def get_cfg() -> Config:\n    \"\"\"Returns the configuration as an object\"\"\"\n    cfg_path = get_cfg_file()\n    with open(cfg_path, 'rb') as fh:\n        cfg_dict = tomli.load(fh)\n    # add config path to later resolve relative paths of config values\n    cfg_dict['cfg_path'] = cfg_path\n    return Config.model_validate(cfg_dict)\n
    "},{"location":"reference/pytanis/config/","title":"Config","text":""},{"location":"reference/pytanis/config/#pytanis.config","title":"config","text":"

    Handling the configuration

    "},{"location":"reference/pytanis/config/#pytanis.config.PYTANIS_CFG_PATH","title":"PYTANIS_CFG_PATH: str = '.pytanis/config.toml' module-attribute","text":"

    Path within $HOME to the configuration file of Pytanis

    "},{"location":"reference/pytanis/config/#pytanis.config.PYTANIS_ENV","title":"PYTANIS_ENV: str = 'PYTANIS_CONFIG' module-attribute","text":"

    Name of the environment variable to look up the path for the config

    "},{"location":"reference/pytanis/config/#pytanis.config.Config","title":"Config","text":"

    Main configuration object

    "},{"location":"reference/pytanis/config/#pytanis.config.Config.Google","title":"Google: GoogleCfg instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.Config.HelpDesk","title":"HelpDesk: HelpDeskCfg instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.Config.Pretalx","title":"Pretalx: PretalxCfg instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.Config.cfg_path","title":"cfg_path: FilePath instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.Config.convert_json_path","title":"convert_json_path(v: GoogleCfg, info: FieldValidationInfo) -> GoogleCfg classmethod","text":"Source code in src/pytanis/config.py
    @field_validator('Google')\n@classmethod\ndef convert_json_path(cls, v: GoogleCfg, info: FieldValidationInfo) -> GoogleCfg:\n    def make_rel_path_abs(entry):\n        if entry is not None and not entry.is_absolute():\n            entry = info.data['cfg_path'].parent / entry\n        return entry\n\n    v.client_secret_json = make_rel_path_abs(v.client_secret_json)\n    v.token_json = make_rel_path_abs(v.token_json)\n\n    return v\n
    "},{"location":"reference/pytanis/config/#pytanis.config.GoogleCfg","title":"GoogleCfg","text":"

    Configuration related to the Google API

    "},{"location":"reference/pytanis/config/#pytanis.config.GoogleCfg.client_secret_json","title":"client_secret_json: Path | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.GoogleCfg.token_json","title":"token_json: Path | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.HelpDeskCfg","title":"HelpDeskCfg","text":"

    Configuration related to the HelpDesk API

    "},{"location":"reference/pytanis/config/#pytanis.config.HelpDeskCfg.account","title":"account: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.HelpDeskCfg.entity_id","title":"entity_id: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.HelpDeskCfg.token","title":"token: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.PretalxCfg","title":"PretalxCfg","text":"

    Configuration related to the Pretalx API

    "},{"location":"reference/pytanis/config/#pytanis.config.PretalxCfg.api_token","title":"api_token: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/config/#pytanis.config.get_cfg","title":"get_cfg() -> Config","text":"

    Returns the configuration as an object

    Source code in src/pytanis/config.py
    def get_cfg() -> Config:\n    \"\"\"Returns the configuration as an object\"\"\"\n    cfg_path = get_cfg_file()\n    with open(cfg_path, 'rb') as fh:\n        cfg_dict = tomli.load(fh)\n    # add config path to later resolve relative paths of config values\n    cfg_dict['cfg_path'] = cfg_path\n    return Config.model_validate(cfg_dict)\n
    "},{"location":"reference/pytanis/config/#pytanis.config.get_cfg_file","title":"get_cfg_file() -> Path","text":"

    Determines the path of the config file

    Source code in src/pytanis/config.py
    def get_cfg_file() -> Path:\n    \"\"\"Determines the path of the config file\"\"\"\n    path_str = os.environ.get(PYTANIS_ENV, None)\n    path = Path.home() / Path(PYTANIS_CFG_PATH) if path_str is None else Path(path_str)\n    return path\n
    "},{"location":"reference/pytanis/google/","title":"Google","text":""},{"location":"reference/pytanis/google/#pytanis.google","title":"google","text":"

    Functionality around the Google's Spreadsheet API

    Additional Documentation

    "},{"location":"reference/pytanis/google/#pytanis.google.ColorType","title":"ColorType = str | tuple[float, float, float] | tuple[float, float, float, float] module-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.__all__","title":"__all__ = ['GSheetClient', 'gsheet_rows_for_fmt', 'PermissionDeniedError'] module-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient","title":"GSheetClient(config: Config | None = None, *, read_only: bool = True)","text":"

    Google API to easily handle GSheets and other files on GDrive

    By default, only the least permissive scope GSHEET_RO in case of read_only = True is used.

    Source code in src/pytanis/google.py
    def __init__(self, config: Config | None = None, *, read_only: bool = True):\n    self._read_only = read_only\n    if read_only:\n        self._scopes = [Scope.GSHEET_RO]\n    else:\n        self._scopes = [Scope.GSHEET_RW]\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    self.gc = gspread_client(self._scopes, config)  # gspread client for more functionality\n
    "},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.gc","title":"gc = gspread_client(self._scopes, config) instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.clear_gsheet","title":"clear_gsheet(spreadsheet_id: str, worksheet_name: str)","text":"

    Clear the worksheet including values, formatting, filtering, etc.

    Source code in src/pytanis/google.py
    def clear_gsheet(self, spreadsheet_id: str, worksheet_name: str):\n    \"\"\"Clear the worksheet including values, formatting, filtering, etc.\"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name, create_ws=False)\n    default_fmt = get_default_format(worksheet.spreadsheet)\n    wrange = worksheet_range(worksheet)\n    try:\n        worksheet.clear()\n        worksheet.clear_basic_filter()\n        format_cell_range(worksheet, wrange, default_fmt)\n        rules = get_conditional_format_rules(worksheet)\n        rules.clear()\n        rules.save()\n        set_data_validation_for_cell_range(worksheet, wrange, None)\n    except APIError as error:\n        self._exception_feedback(error)\n
    "},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.gsheet","title":"gsheet(spreadsheet_id: str, worksheet_name: str | None = None, *, create_ws: bool = False) -> Worksheet | Spreadsheet","text":"

    Retrieve a Google sheet by its id and the name

    Open a Google sheet in your browser and check the URL to retrieve the id, e.g.: https://docs.google.com/spreadsheets/d/SPREEDSHEET_ID/edit...

    If the spreadsheet as several worksheets (check the lower bar) then worksheet_name can be used to specify a specific one.

    Source code in src/pytanis/google.py
    def gsheet(\n    self, spreadsheet_id: str, worksheet_name: str | None = None, *, create_ws: bool = False\n) -> Worksheet | Spreadsheet:\n    \"\"\"Retrieve a Google sheet by its id and the name\n\n    Open a Google sheet in your browser and check the URL to retrieve the id, e.g.:\n    https://docs.google.com/spreadsheets/d/SPREEDSHEET_ID/edit...\n\n    If the spreadsheet as several worksheets (check the lower bar) then `worksheet_name` can be used to\n    specify a specific one.\n    \"\"\"\n    spreadsheet = self.gc.open_by_key(spreadsheet_id)\n    if worksheet_name is None:\n        return spreadsheet\n    elif worksheet_name in [ws.title for ws in spreadsheet.worksheets()]:\n        return spreadsheet.worksheet(worksheet_name)\n    elif create_ws:\n        worksheet = spreadsheet.add_worksheet(title=worksheet_name, rows=100, cols=20)\n        self._wait_for_worksheet(spreadsheet_id, worksheet_name)\n        return worksheet\n    else:\n        return spreadsheet.worksheet(worksheet_name)  # raises exception\n
    "},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.gsheet_as_df","title":"gsheet_as_df(spreadsheet_id: str, worksheet_name: str, **kwargs: str | bool | int) -> pd.DataFrame","text":"

    Returns a worksheet as dataframe

    Source code in src/pytanis/google.py
    def gsheet_as_df(self, spreadsheet_id: str, worksheet_name: str, **kwargs: str | (bool | int)) -> pd.DataFrame:\n    \"\"\"Returns a worksheet as dataframe\"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name)\n    df = get_as_dataframe(worksheet, **kwargs)\n    # remove Nan rows & columns as they are exported by default\n    df.dropna(how='all', inplace=True, axis=0)\n    df.dropna(how='all', inplace=True, axis=1)\n    return df\n
    "},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.recreate_token","title":"recreate_token()","text":"

    Recreate the current token using the scopes given at initialization

    Source code in src/pytanis/google.py
    def recreate_token(self):\n    \"\"\"Recreate the current token using the scopes given at initialization\"\"\"\n    self._config.Google.token_json.unlink(missing_ok=True)\n    self.gc = gspread_client(self._scopes, self._config)\n
    "},{"location":"reference/pytanis/google/#pytanis.google.GSheetClient.save_df_as_gsheet","title":"save_df_as_gsheet(df: pd.DataFrame, spreadsheet_id: str, worksheet_name: str, *, create_ws: bool = False, default_fmt: bool = True, **kwargs: str | bool | int)","text":"

    Save the given dataframe as worksheet in a spreadsheet

    Make sure that the scope passed gives you write permissions

    Parameters:

    Name Type Description Default df DataFrame

    dataframe to save

    required spreadsheet_id str

    id of the Google spreadsheet

    required worksheet_name str

    name of the worksheet within the spreadsheet

    required create_ws bool

    create the worksheet if non-existent

    False default_fmt bool

    apply default formatter BasicFormatter

    True **kwargs str | bool | int

    extra keyword arguments passed to set_with_dataframe

    {} Source code in src/pytanis/google.py
    def save_df_as_gsheet(\n    self,\n    df: pd.DataFrame,\n    spreadsheet_id: str,\n    worksheet_name: str,\n    *,\n    create_ws: bool = False,\n    default_fmt: bool = True,\n    **kwargs: str | (bool | int),\n):\n    \"\"\"Save the given dataframe as worksheet in a spreadsheet\n\n    Make sure that the scope passed gives you write permissions\n\n    Args:\n        df: dataframe to save\n        spreadsheet_id: id of the Google spreadsheet\n        worksheet_name: name of the worksheet within the spreadsheet\n        create_ws: create the worksheet if non-existent\n        default_fmt: apply default formatter `BasicFormatter`\n        **kwargs: extra keyword arguments passed to `set_with_dataframe`\n    \"\"\"\n    worksheet = self.gsheet(spreadsheet_id, worksheet_name, create_ws=create_ws)\n    # make sure it's really only the dataframe, not some residue\n    self.clear_gsheet(spreadsheet_id, worksheet_name)\n    params = {'resize': True} | dict(**kwargs)  # set sane defaults\n    try:\n        set_with_dataframe(worksheet, df, **params)\n        if default_fmt:\n            format_with_dataframe(worksheet, df)\n    except APIError as error:\n        self._exception_feedback(error)\n
    "},{"location":"reference/pytanis/google/#pytanis.google.PermissionDeniedError","title":"PermissionDeniedError","text":"

    Error for APIError with status PERMISSION_DENIED

    Most likely thrown in cases when the scope is not GSHEET_RW or the token needs to be updated accordingly.

    "},{"location":"reference/pytanis/google/#pytanis.google.Scope","title":"Scope","text":""},{"location":"reference/pytanis/google/#pytanis.google.Scope.GDRIVE_FILE","title":"GDRIVE_FILE = 'https://www.googleapis.com/auth/drive.file' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.Scope.GDRIVE_RO","title":"GDRIVE_RO = 'https://www.googleapis.com/auth/drive.readonly' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.Scope.GDRIVE_RW","title":"GDRIVE_RW = 'https://www.googleapis.com/auth/drive' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.Scope.GSHEET_RO","title":"GSHEET_RO = 'https://www.googleapis.com/auth/spreadsheets.readonly' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.Scope.GSHEET_RW","title":"GSHEET_RW = 'https://www.googleapis.com/auth/spreadsheets' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/google/#pytanis.google.gsheet_col","title":"gsheet_col(idx: int) -> str","text":"

    Convert a column index to Google Sheet range notation, e.g. A, BE, etc.

    Source code in src/pytanis/google.py
    def gsheet_col(idx: int) -> str:\n    \"\"\"Convert a column index to Google Sheet range notation, e.g. A, BE, etc.\"\"\"\n    idx += 1\n    chars = []\n    while idx:\n        chars.append(string.ascii_uppercase[(idx % 26) - 1])\n        idx //= 27\n    return ''.join(chars[::-1])\n
    "},{"location":"reference/pytanis/google/#pytanis.google.gsheet_rows_for_fmt","title":"gsheet_rows_for_fmt(mask: pd.Series, n_cols: int) -> list[str]","text":"

    Get the Google Sheet row range specifications for formatting

    Source code in src/pytanis/google.py
    def gsheet_rows_for_fmt(mask: pd.Series, n_cols: int) -> list[str]:\n    \"\"\"Get the Google Sheet row range specifications for formatting\"\"\"\n    rows = pd.Series(np.argwhere(mask.to_numpy()).reshape(-1) + 2)  # +2 since 1-index and header\n    last_col = gsheet_col(n_cols - 1)  # last index\n    rows = rows.map(lambda x: f'A{x}:{last_col}{x}')\n    return rows.to_list()\n
    "},{"location":"reference/pytanis/google/#pytanis.google.gspread_client","title":"gspread_client(scopes: list[Scope], config: Config) -> gspread.client.Client","text":"

    Creates the GSheet client using our configuration

    Read GSpread for usage details

    Source code in src/pytanis/google.py
    def gspread_client(scopes: list[Scope], config: Config) -> gspread.client.Client:\n    \"\"\"Creates the GSheet client using our configuration\n\n    Read [GSpread](https://docs.gspread.org/) for usage details\n    \"\"\"\n    if (secret_path := config.Google.client_secret_json) is None:\n        msg = 'You have to set Google.client_secret_json in your config.toml!'\n        raise RuntimeError(msg)\n    if (token_path := config.Google.token_json) is None:\n        msg = 'You have to set Google.token_json in your config.toml!'\n        raise RuntimeError(msg)\n\n    gc = gspread.oauth(\n        scopes=[scope.value for scope in scopes],\n        credentials_filename=str(secret_path),\n        authorized_user_filename=str(token_path),\n    )\n    return gc\n
    "},{"location":"reference/pytanis/google/#pytanis.google.mark_rows","title":"mark_rows(worksheet, mask: pd.Series, color: ColorType)","text":"

    Mark rows specified by a mask (condition) with a given color

    Color can be a tuple of RGB values or a Matplotlib string specification: https://matplotlib.org/stable/gallery/color/named_colors.html#css-colors

    Source code in src/pytanis/google.py
    def mark_rows(worksheet, mask: pd.Series, color: ColorType):\n    \"\"\"Mark rows specified by a mask (condition) with a given color\n\n    Color can be a tuple of RGB values or a Matplotlib string specification:\n    https://matplotlib.org/stable/gallery/color/named_colors.html#css-colors\n    \"\"\"\n    rows = gsheet_rows_for_fmt(mask, worksheet.col_count)\n    fmt = cellFormat(backgroundColor=Color(*to_rgb(color)))\n    if rows:\n        format_cell_ranges(worksheet, [(rng, fmt) for rng in rows])\n
    "},{"location":"reference/pytanis/google/#pytanis.google.worksheet_range","title":"worksheet_range(worksheet: Worksheet) -> str","text":"

    Returns a range encompassing the whole worksheet

    Source code in src/pytanis/google.py
    def worksheet_range(worksheet: Worksheet) -> str:\n    \"\"\"Returns a range encompassing the whole worksheet\"\"\"\n    last_row = worksheet.row_count\n    last_col = gsheet_col(worksheet.col_count)\n    return f'A1:{last_col}{last_row}'\n
    "},{"location":"reference/pytanis/highs/","title":"Highs","text":""},{"location":"reference/pytanis/highs/#pytanis.highs","title":"highs","text":"

    Some helper functions for HiGHS (https://highs.dev/)

    pyomo and highspy need to be installed, consider pip install 'pytanis[all]'.

    ToDo

    "},{"location":"reference/pytanis/highs/#pytanis.highs.read_sol_file","title":"read_sol_file(file_name: str) -> Iterator[tuple[str, float]]","text":"

    Read a solution file from HiGHS solver with default output style

    We assume here that your variable names are alphanumeric!

    No underscores, no dashes, etc.!

    Source code in src/pytanis/highs.py
    def read_sol_file(file_name: str) -> Iterator[tuple[str, float]]:\n    \"\"\"Read a solution file from HiGHS solver with default output style\n\n    Attention: We assume here that your variable names are alphanumeric!\n               No underscores, no dashes, etc.!\n    \"\"\"\n    line_re = re.compile(r'(\\w+)(?:\\((\\w+)\\))?(_binary_indicator_var)? ([.\\w-]+)')\n\n    with open(file_name, encoding='utf8') as fh:\n        while True:\n            line = fh.readline()\n            if line.startswith('# Columns'):\n                break\n        for line in fh.readlines():\n            if line.startswith('#'):\n                break\n            if (match_obj := line_re.match(line.strip())) is None:\n                msg = f'Could not interpret line: {line}'\n                raise RuntimeError(msg)\n            else:\n                var_name, idx, binary, val = match_obj.groups()\n            val = float(val)\n            binary = binary.replace('_', '.', 1) if binary else ''\n\n            if idx is None:\n                yield f'{var_name}{binary}', val\n            else:\n                idx = idx.replace('_', ',')\n                yield f'{var_name}[{idx}]{binary}', val\n
    "},{"location":"reference/pytanis/highs/#pytanis.highs.set_solution_from_file","title":"set_solution_from_file(model: ConcreteModel, file_name: str)","text":"

    Given a HiGHS solution file set the variables of a Pyomo model accordingly.

    This is a workaround to set a Pyomo model's variables to the solution from a HiGHS solution file.

    Source code in src/pytanis/highs.py
    def set_solution_from_file(model: ConcreteModel, file_name: str):\n    \"\"\"Given a HiGHS solution file set the variables of a Pyomo model accordingly.\n\n    This is a workaround to set a Pyomo model's variables to the solution\n    from a HiGHS solution file.\n    \"\"\"\n    # just to initialize we read it in using HiGHS. The result is incorrect though,\n    # as the order of variables is mixed up quite often. We fix this below!\n    opt = Highs()\n    opt.set_instance(model)\n    opt._solver_model.readSolution(file_name, 0)\n    opt._sol = opt._solver_model.getSolution()\n    opt.load_vars()\n\n    # read the actual mapping of the variable names to the values\n    file_sol = dict(read_sol_file(file_name))\n\n    # overwrite the values of the variables again using the symbolic names from the file\n    for v, ref_info in opt._referenced_variables.items():\n        using_cons, using_sos, using_obj = ref_info\n        if using_cons or using_sos or (using_obj is not None):\n            var = opt._vars[v][0]\n            var.set_value(file_sol[var.name], skip_validation=True)\n
    "},{"location":"reference/pytanis/review/","title":"Review","text":""},{"location":"reference/pytanis/review/#pytanis.review","title":"review","text":"

    Tools related to assigning proposals to reviewers

    In Pretalx assignments can be done in two directions:

    1. Assign proposals to reviewers
    2. Assign reviewers to proposals

    We will always assume direction 1. in this file when we talk about an assignment. So in Operation Research-speak, resources get assigned tasks, not the other way around. The time needed for the task of reviewing a proposal is quite homogeneous while the number of reviews a single reviewer may highly vary. Also, we will rather use the name submission instead of proposal as this also reflects the naming of the Pretalx API.

    We follow the convention over configuration principle here and thus check out the Col class for the naming of columns.

    "},{"location":"reference/pytanis/review/#pytanis.review.Col","title":"Col","text":"

    Additional conventions used for reviews

    "},{"location":"reference/pytanis/review/#pytanis.review.Col.address_as","title":"address_as = 'Address as' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.all_proposals","title":"all_proposals = 'All Proposals' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.committee_contact","title":"committee_contact = 'Committee Contact' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.committee_member","title":"committee_member = 'Committee Member' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.curr_assignments","title":"curr_assignments = 'Current Assignments' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.done_nreviews","title":"done_nreviews = 'Done #Reviews' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.nassignments","title":"nassignments = '#Assignments' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.nvotes","title":"nvotes = '#Votes' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.pretalx_activated","title":"pretalx_activated = 'Pretalx activated' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.rem_nreviews","title":"rem_nreviews = 'Remaining #Reviews' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.target_nreviews","title":"target_nreviews = 'Target #Reviews' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.track_prefs","title":"track_prefs = 'Track Preferences' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.Col.vote_score","title":"vote_score = 'Vote Score' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/review/#pytanis.review.read_assignment_as_df","title":"read_assignment_as_df(file_path: Path) -> pd.DataFrame","text":"

    Reads an assignment and returns a dataframe

    Source code in src/pytanis/review.py
    def read_assignment_as_df(file_path: Path) -> pd.DataFrame:\n    \"\"\"Reads an assignment and returns a dataframe\"\"\"\n    with open(file_path, encoding='utf8') as fh:\n        curr_assign = json.load(fh)\n    df = pd.DataFrame({k: [v] for k, v in curr_assign.items()})\n    df = df.T.rename_axis(index=Col.email).rename(columns={0: Col.curr_assignments}).reset_index()\n    return df\n
    "},{"location":"reference/pytanis/review/#pytanis.review.save_assignments_as_json","title":"save_assignments_as_json(df: pd.DataFrame, file_path: Path | str)","text":"

    Save the dataframe as proposal assignment JSON file

    Source code in src/pytanis/review.py
    def save_assignments_as_json(df: pd.DataFrame, file_path: Path | str):\n    \"\"\"Save the dataframe as proposal assignment JSON file\"\"\"\n    file_path = Path(file_path)\n    df = df.loc[:, [Col.email, Col.curr_assignments]]\n    json_dct = json.loads(df.set_index(Col.email).to_json())[Col.curr_assignments]\n    # prettify the json string for human-edit-ability if reviewers need to be dropped later\n    json_str = json.dumps(json_dct).replace('{', '{\\n').replace('], ', '],\\n').replace(']}', ']\\n}')\n    with open(file_path, 'w', encoding='utf8') as fh:\n        fh.write(json_str)\n
    "},{"location":"reference/pytanis/utils/","title":"Utils","text":""},{"location":"reference/pytanis/utils/#pytanis.utils","title":"utils","text":"

    Additional utilities

    "},{"location":"reference/pytanis/utils/#pytanis.utils.RT","title":"RT = TypeVar('RT') module-attribute","text":""},{"location":"reference/pytanis/utils/#pytanis.utils.implode","title":"implode(df: pd.DataFrame, cols: str | list[str]) -> pd.DataFrame","text":"

    The inverse of Pandas' explode

    Source code in src/pytanis/utils.py
    def implode(df: pd.DataFrame, cols: str | list[str]) -> pd.DataFrame:\n    \"\"\"The inverse of Pandas' explode\"\"\"\n    if not isinstance(cols, list):\n        cols = [cols]\n    orig_cols = df.columns\n    grp_cols = [col for col in df.columns if col not in cols]\n    df = df.groupby(grp_cols, group_keys=True, dropna=False).aggregate({col: lambda x: x.tolist() for col in cols})\n    df.reset_index(inplace=True)\n    df = df.loc[:, orig_cols]\n    return df\n
    "},{"location":"reference/pytanis/utils/#pytanis.utils.pretty_timedelta","title":"pretty_timedelta(seconds: int) -> str","text":"

    Converts timedelta in seconds to human-readable string

    Parameters:

    Name Type Description Default seconds int

    time delta in seconds

    required

    Returns:

    Type Description str

    timedelta as pretty string

    Source code in src/pytanis/utils.py
    def pretty_timedelta(seconds: int) -> str:\n    \"\"\"Converts timedelta in seconds to human-readable string\n\n    Args:\n        seconds: time delta in seconds\n\n    Returns:\n        timedelta as pretty string\n    \"\"\"\n    sign = '-' if seconds < 0 else ''\n    seconds = abs(int(seconds))\n    days, seconds = divmod(seconds, 86400)\n    hours, seconds = divmod(seconds, 3600)\n    minutes, seconds = divmod(seconds, 60)\n    if days > 0:\n        return f'{sign}{days}d{hours}h{minutes}m{seconds}s'\n    elif hours > 0:\n        return f'{sign}{hours}h{minutes}m{seconds}s'\n    elif minutes > 0:\n        return f'{sign}{minutes}m{seconds}s'\n    else:\n        return f'{sign}{seconds}s'\n
    "},{"location":"reference/pytanis/utils/#pytanis.utils.rm_keys","title":"rm_keys(keys: Any | list[Any], dct: dict[Any, Any]) -> dict[Any, Any]","text":"

    Return a copy with keys removed from dictionary

    Source code in src/pytanis/utils.py
    def rm_keys(\n    keys: Any | list[Any],\n    dct: dict[Any, Any],\n) -> dict[Any, Any]:\n    \"\"\"Return a copy with keys removed from dictionary\"\"\"\n    if not isinstance(keys, list):\n        keys = [keys]\n    return {k: v for k, v in dct.items() if k not in keys}\n
    "},{"location":"reference/pytanis/utils/#pytanis.utils.throttle","title":"throttle(calls: int, seconds: int = 1) -> Callable[[Callable[..., RT]], Callable[..., RT]]","text":"

    Decorator for throttling a function to number of calls per seconds

    Parameters:

    Name Type Description Default calls int

    number of calls per interval

    required seconds int

    number of seconds in interval

    1

    Returns:

    Type Description Callable[[Callable[..., RT]], Callable[..., RT]]

    wrapped function

    Source code in src/pytanis/utils.py
    def throttle(calls: int, seconds: int = 1) -> Callable[[Callable[..., RT]], Callable[..., RT]]:\n    \"\"\"Decorator for throttling a function to number of calls per seconds\n\n    Args:\n        calls: number of calls per interval\n        seconds: number of seconds in interval\n\n    Returns:\n        wrapped function\n    \"\"\"\n    if not isinstance(calls, int):\n        msg = 'number of calls must be integer'\n        raise ValueError(msg)\n    if not isinstance(seconds, int):\n        msg = 'number of seconds must be integer'\n        raise ValueError(msg)\n\n    def decorator(func: Callable[..., RT]) -> Callable[..., RT]:\n        # keeps track of the last calls\n        last_calls: list[float] = []\n        lock = threading.Lock()\n\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs) -> RT:\n            nonlocal last_calls\n            with lock:\n                curr_time = time.time()\n                # Remove old calls\n                last_calls = [call for call in last_calls if call > curr_time - seconds]\n\n                if len(last_calls) >= calls:\n                    sleep_time = last_calls[0] + seconds - curr_time\n                    logger = get_logger()\n                    logger.debug('stalling call', func=func.__name__, secs=sleep_time)\n                    time.sleep(sleep_time)\n\n                resp = func(*args, **kwargs)\n                last_calls.append(time.time())\n                return resp\n\n        return wrapper\n\n    return decorator\n
    "},{"location":"reference/pytanis/helpdesk/","title":"Helpdesk","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk","title":"helpdesk","text":"

    Functionality around the HelpDesk / LiveChat API

    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.__all__","title":"__all__ = ['HelpDeskClient', 'Mail', 'MailClient', 'Recipient'] module-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient","title":"HelpDeskClient(config: Config | None = None)","text":"Source code in src/pytanis/helpdesk/client.py
    def __init__(self, config: Config | None = None):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    # Important: Always use a custom User-Agent, never a generic one.\n    # Generic User-Agents are filtered by helpdesk to reduce spam.\n    self._headers = {'User-Agent': 'Pytanis'}\n\n    self._get_throttled = self._get\n    self._post_throttled = self._post\n    self.set_throttling(calls=1, seconds=2)  # Helpdesk is really strange when it comes to this\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.create_ticket","title":"create_ticket(ticket: NewTicket)","text":"Source code in src/pytanis/helpdesk/client.py
    def create_ticket(self, ticket: NewTicket):\n    return self.post('tickets', data=ticket.model_dump())\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.get","title":"get(endpoint: str, params: QueryParams | None = None) -> JSON","text":"

    Retrieve data via throttled GET request and return the JSON

    Source code in src/pytanis/helpdesk/client.py
    def get(self, endpoint: str, params: QueryParams | None = None) -> JSON:\n    \"\"\"Retrieve data via throttled GET request and return the JSON\"\"\"\n    resp = self._get_throttled(endpoint, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.list_agents","title":"list_agents() -> list[Agent]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_agents(self) -> list[Agent]:\n    agents = self.get('agents')\n    if not isinstance(agents, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Agent.model_validate(dct) for dct in agents]\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.list_teams","title":"list_teams() -> list[Team]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_teams(self) -> list[Team]:\n    teams = self.get('teams')\n    if not isinstance(teams, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Team.model_validate(dct) for dct in teams]\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.post","title":"post(endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON","text":"Source code in src/pytanis/helpdesk/client.py
    def post(self, endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON:\n    resp = self._post_throttled(endpoint, data, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.HelpDeskClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/helpdesk/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.debug('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n    self._post_throttled = throttle(calls, seconds)(self._post)\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail","title":"Mail","text":"

    Mail template

    Use the data field to store additional information

    You can use the typical Format String Syntax and the objects recipient and mail to access metadata to complement the template, e.g.:

    Hello {recipient.address_as},\n\nWe hope it's ok to address you your first name rather than using your full name being {recipient.name}.\nHave you read the email's subject '{mail.subject}'? How is your work right now at {recipient.data.company}?\n\nCheers!\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.agent_id","title":"agent_id: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.data","title":"data: MetaData | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.recipients","title":"recipients: list[Recipient] instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.status","title":"status: str = 'solved' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.subject","title":"subject: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.team_id","title":"team_id: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Mail.text","title":"text: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient","title":"MailClient(helpdesk_client: HelpDeskClient | None = None)","text":"

    Mail client for mass mails over HelpDesk

    Source code in src/pytanis/helpdesk/mail.py
    def __init__(self, helpdesk_client: HelpDeskClient | None = None):\n    if helpdesk_client is None:\n        helpdesk_client = HelpDeskClient()\n    self._helpdesk_client = helpdesk_client\n    self.dry_run: Callable[[NewTicket], None] = self.print_new_ticket\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient.batch_size","title":"batch_size: int = 20 class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient.dry_run","title":"dry_run: Callable[[NewTicket], None] = self.print_new_ticket instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient.wait_time","title":"wait_time: int = 30 class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient.print_new_ticket","title":"print_new_ticket(ticket: NewTicket) staticmethod","text":"

    Default action in a dry-run. Mainly for making sure you sent what you mean!

    Overwrite it by assigning to self.dry_run another function

    ToDo: Make this function nice, maybe use the rich library even

    Source code in src/pytanis/helpdesk/mail.py
    @staticmethod\ndef print_new_ticket(ticket: NewTicket):\n    \"\"\"Default action in a dry-run. Mainly for making sure you sent what you mean!\n\n    Overwrite it by assigning to self.dry_run another function\n\n    ToDo: Make this function nice, maybe use the `rich` library even\n    \"\"\"\n    print('#' * 40)  # noqa: T201\n    print(f'Recipient: {ticket.requester.name} <{ticket.requester.email}>')  # noqa: T201\n    print(f'Subject: {ticket.subject}')  # noqa: T201\n    print(f'{ticket.message.text}')  # noqa: T201\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.MailClient.send","title":"send(mail: Mail, *, dry_run: bool = True) -> tuple[list[tuple[Recipient, Ticket | None]], list[tuple[Recipient, Exception]]]","text":"

    Send a mail to all recipients using HelpDesk

    Source code in src/pytanis/helpdesk/mail.py
    def send(\n    self, mail: Mail, *, dry_run: bool = True\n) -> tuple[list[tuple[Recipient, Ticket | None]], list[tuple[Recipient, Exception]]]:\n    \"\"\"Send a mail to all recipients using HelpDesk\"\"\"\n    errors = []\n    tickets = []\n    for idx, recipient in enumerate(tqdm(mail.recipients), start=1):\n        recip_mail = mail.model_copy()\n        try:\n            recip_mail.subject = mail.subject.format(recipient=recipient, mail=mail)\n            # be aware here that the body might reference to subject line, so it must be filled already\n            recip_mail.text = recip_mail.text.format(recipient=recipient, mail=recip_mail)\n            ticket = self._create_ticket(recip_mail, recipient)\n            if dry_run:\n                self.print_new_ticket(ticket)\n                resp_ticket = None\n            else:\n                resp = self._helpdesk_client.create_ticket(ticket)\n                resp_ticket = Ticket.model_validate(resp)\n        except Exception as e:\n            errors.append((recipient, e))\n        else:\n            tickets.append((recipient, resp_ticket))\n        if idx % self.batch_size == 0:\n            time.sleep(self.wait_time)\n\n    return tickets, errors\n
    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient","title":"Recipient","text":"

    Details about the recipient

    Use the data field to store additional information

    "},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient.address_as","title":"address_as: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient.data","title":"data: MetaData | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient.email","title":"email: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/#pytanis.helpdesk.Recipient.fill_with_name","title":"fill_with_name(v, values) classmethod","text":"Source code in src/pytanis/helpdesk/mail.py
    @validator('address_as')\n@classmethod\ndef fill_with_name(cls, v, values):\n    if v is None:\n        v = values['name']\n    return v\n
    "},{"location":"reference/pytanis/helpdesk/client/","title":"Client","text":""},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client","title":"client","text":"

    Client for the HelpDesk / LiveChat API

    Documentation: https://api.helpdesk.com/docs

    ToDo

    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.JSON","title":"JSON: TypeAlias = JSONObj | JSONLst module-attribute","text":"

    Type of the JSON response as returned by the HelpDesk / LiveChat API

    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.JSONLst","title":"JSONLst: TypeAlias = list[JSONObj] module-attribute","text":"

    Type of a JSON list of JSON objects

    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.JSONObj","title":"JSONObj: TypeAlias = dict[str, Any] module-attribute","text":"

    Type of a JSON object (without recursion)

    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient","title":"HelpDeskClient(config: Config | None = None)","text":"Source code in src/pytanis/helpdesk/client.py
    def __init__(self, config: Config | None = None):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    # Important: Always use a custom User-Agent, never a generic one.\n    # Generic User-Agents are filtered by helpdesk to reduce spam.\n    self._headers = {'User-Agent': 'Pytanis'}\n\n    self._get_throttled = self._get\n    self._post_throttled = self._post\n    self.set_throttling(calls=1, seconds=2)  # Helpdesk is really strange when it comes to this\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.create_ticket","title":"create_ticket(ticket: NewTicket)","text":"Source code in src/pytanis/helpdesk/client.py
    def create_ticket(self, ticket: NewTicket):\n    return self.post('tickets', data=ticket.model_dump())\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.get","title":"get(endpoint: str, params: QueryParams | None = None) -> JSON","text":"

    Retrieve data via throttled GET request and return the JSON

    Source code in src/pytanis/helpdesk/client.py
    def get(self, endpoint: str, params: QueryParams | None = None) -> JSON:\n    \"\"\"Retrieve data via throttled GET request and return the JSON\"\"\"\n    resp = self._get_throttled(endpoint, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.list_agents","title":"list_agents() -> list[Agent]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_agents(self) -> list[Agent]:\n    agents = self.get('agents')\n    if not isinstance(agents, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Agent.model_validate(dct) for dct in agents]\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.list_teams","title":"list_teams() -> list[Team]","text":"Source code in src/pytanis/helpdesk/client.py
    def list_teams(self) -> list[Team]:\n    teams = self.get('teams')\n    if not isinstance(teams, list):\n        msg = 'Received JSON is not a list object'\n        raise ValueError(msg)\n    return [Team.model_validate(dct) for dct in teams]\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.post","title":"post(endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON","text":"Source code in src/pytanis/helpdesk/client.py
    def post(self, endpoint: str, data: dict[str, Any], params: QueryParams | None = None) -> JSON:\n    resp = self._post_throttled(endpoint, data, params)\n    resp.raise_for_status()\n    return resp.json()\n
    "},{"location":"reference/pytanis/helpdesk/client/#pytanis.helpdesk.client.HelpDeskClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/helpdesk/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.debug('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n    self._post_throttled = throttle(calls, seconds)(self._post)\n
    "},{"location":"reference/pytanis/helpdesk/mail/","title":"Mail","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail","title":"mail","text":"

    Functionality around mailing

    ToDo

    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail","title":"Mail","text":"

    Mail template

    Use the data field to store additional information

    You can use the typical Format String Syntax and the objects recipient and mail to access metadata to complement the template, e.g.:

    Hello {recipient.address_as},\n\nWe hope it's ok to address you your first name rather than using your full name being {recipient.name}.\nHave you read the email's subject '{mail.subject}'? How is your work right now at {recipient.data.company}?\n\nCheers!\n
    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.agent_id","title":"agent_id: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.data","title":"data: MetaData | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.recipients","title":"recipients: list[Recipient] instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.status","title":"status: str = 'solved' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.subject","title":"subject: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.team_id","title":"team_id: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Mail.text","title":"text: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient","title":"MailClient(helpdesk_client: HelpDeskClient | None = None)","text":"

    Mail client for mass mails over HelpDesk

    Source code in src/pytanis/helpdesk/mail.py
    def __init__(self, helpdesk_client: HelpDeskClient | None = None):\n    if helpdesk_client is None:\n        helpdesk_client = HelpDeskClient()\n    self._helpdesk_client = helpdesk_client\n    self.dry_run: Callable[[NewTicket], None] = self.print_new_ticket\n
    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient.batch_size","title":"batch_size: int = 20 class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient.dry_run","title":"dry_run: Callable[[NewTicket], None] = self.print_new_ticket instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient.wait_time","title":"wait_time: int = 30 class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient.print_new_ticket","title":"print_new_ticket(ticket: NewTicket) staticmethod","text":"

    Default action in a dry-run. Mainly for making sure you sent what you mean!

    Overwrite it by assigning to self.dry_run another function

    ToDo: Make this function nice, maybe use the rich library even

    Source code in src/pytanis/helpdesk/mail.py
    @staticmethod\ndef print_new_ticket(ticket: NewTicket):\n    \"\"\"Default action in a dry-run. Mainly for making sure you sent what you mean!\n\n    Overwrite it by assigning to self.dry_run another function\n\n    ToDo: Make this function nice, maybe use the `rich` library even\n    \"\"\"\n    print('#' * 40)  # noqa: T201\n    print(f'Recipient: {ticket.requester.name} <{ticket.requester.email}>')  # noqa: T201\n    print(f'Subject: {ticket.subject}')  # noqa: T201\n    print(f'{ticket.message.text}')  # noqa: T201\n
    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MailClient.send","title":"send(mail: Mail, *, dry_run: bool = True) -> tuple[list[tuple[Recipient, Ticket | None]], list[tuple[Recipient, Exception]]]","text":"

    Send a mail to all recipients using HelpDesk

    Source code in src/pytanis/helpdesk/mail.py
    def send(\n    self, mail: Mail, *, dry_run: bool = True\n) -> tuple[list[tuple[Recipient, Ticket | None]], list[tuple[Recipient, Exception]]]:\n    \"\"\"Send a mail to all recipients using HelpDesk\"\"\"\n    errors = []\n    tickets = []\n    for idx, recipient in enumerate(tqdm(mail.recipients), start=1):\n        recip_mail = mail.model_copy()\n        try:\n            recip_mail.subject = mail.subject.format(recipient=recipient, mail=mail)\n            # be aware here that the body might reference to subject line, so it must be filled already\n            recip_mail.text = recip_mail.text.format(recipient=recipient, mail=recip_mail)\n            ticket = self._create_ticket(recip_mail, recipient)\n            if dry_run:\n                self.print_new_ticket(ticket)\n                resp_ticket = None\n            else:\n                resp = self._helpdesk_client.create_ticket(ticket)\n                resp_ticket = Ticket.model_validate(resp)\n        except Exception as e:\n            errors.append((recipient, e))\n        else:\n            tickets.append((recipient, resp_ticket))\n        if idx % self.batch_size == 0:\n            time.sleep(self.wait_time)\n\n    return tickets, errors\n
    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.MetaData","title":"MetaData","text":"

    Additional, arbitrary metadata provided by the user like for template filling

    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient","title":"Recipient","text":"

    Details about the recipient

    Use the data field to store additional information

    "},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient.address_as","title":"address_as: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient.data","title":"data: MetaData | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient.email","title":"email: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/mail/#pytanis.helpdesk.mail.Recipient.fill_with_name","title":"fill_with_name(v, values) classmethod","text":"Source code in src/pytanis/helpdesk/mail.py
    @validator('address_as')\n@classmethod\ndef fill_with_name(cls, v, values):\n    if v is None:\n        v = values['name']\n    return v\n
    "},{"location":"reference/pytanis/helpdesk/types/","title":"Types","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types","title":"types","text":"

    Return types of the HelpDesk / LiveChat API

    Documentation: https://api.helpdesk.com/docs

    ToDo

    "},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Agent","title":"Agent","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Assignment","title":"Assignment","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Assignment.agent","title":"agent: Id instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Assignment.team","title":"team: Id instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Id","title":"Id","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Id.ID","title":"ID: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Message","title":"Message","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Message.text","title":"text: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket","title":"NewTicket","text":"

    Object that needs to be sent when creating a NEW ticket

    "},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.assignment","title":"assignment: Assignment | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.message","title":"message: Message instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.requester","title":"requester: Requester instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.status","title":"status: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.subject","title":"subject: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.NewTicket.teamIDs","title":"teamIDs: list[str] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Requester","title":"Requester","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Requester.email","title":"email: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Requester.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Team","title":"Team","text":""},{"location":"reference/pytanis/helpdesk/types/#pytanis.helpdesk.types.Ticket","title":"Ticket","text":"

    Actual ticket as returned by the API

    "},{"location":"reference/pytanis/pretalx/","title":"Pretalx","text":""},{"location":"reference/pytanis/pretalx/#pytanis.pretalx","title":"pretalx","text":"

    Functionality around the Pretalx API

    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.__all__","title":"__all__ = ['PretalxClient', 'subs_as_df', 'speakers_as_df', 'reviews_as_df'] module-attribute","text":""},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient","title":"PretalxClient(config: Config | None = None, *, blocking: bool = False)","text":"

    Client for the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def __init__(self, config: Config | None = None, *, blocking: bool = False):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    self._get_throttled = self._get\n    self.blocking = blocking\n    self.set_throttling(calls=2, seconds=1)  # we are nice by default and Pretalx doesn't allow many calls at once.\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.blocking","title":"blocking = blocking instance-attribute","text":""},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.answer","title":"answer(event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer","text":"

    Returns a specific answer

    Source code in src/pytanis/pretalx/client.py
    def answer(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer:  # noqa: A002\n    \"\"\"Returns a specific answer\"\"\"\n    return self._endpoint_id(Answer, event_slug, 'answers', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.answers","title":"answers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]","text":"

    Lists all answers and their details

    Source code in src/pytanis/pretalx/client.py
    def answers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]:\n    \"\"\"Lists all answers and their details\"\"\"\n    return self._endpoint_lst(Answer, event_slug, 'answers', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.event","title":"event(event_slug: str, *, params: QueryParams | None = None) -> Event","text":"

    Returns detailed information about a specific event

    Source code in src/pytanis/pretalx/client.py
    def event(self, event_slug: str, *, params: QueryParams | None = None) -> Event:\n    \"\"\"Returns detailed information about a specific event\"\"\"\n    endpoint = f'/api/events/{event_slug}/'\n    result = self._get_one(endpoint, params)\n    _logger.debug('result', resp=result)\n    return Event.model_validate(result)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.events","title":"events(*, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]","text":"

    Lists all events and their details

    Source code in src/pytanis/pretalx/client.py
    def events(self, *, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]:\n    \"\"\"Lists all events and their details\"\"\"\n    count, results = self._get_many('/api/events/', params)\n    events = iter(_logger.debug('result', resp=r) or Event.model_validate(r) for r in results)\n    return count, events\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.me","title":"me() -> Me","text":"

    Returns what Pretalx knows about myself

    Source code in src/pytanis/pretalx/client.py
    def me(self) -> Me:\n    \"\"\"Returns what Pretalx knows about myself\"\"\"\n    result = self._get_one('/api/me')\n    return Me.model_validate(result)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.question","title":"question(event_slug: str, id: int, *, params: QueryParams | None = None) -> Question","text":"

    Returns a specific question

    Source code in src/pytanis/pretalx/client.py
    def question(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Question:  # noqa: A002\n    \"\"\"Returns a specific question\"\"\"\n    return self._endpoint_id(Question, event_slug, 'questions', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.questions","title":"questions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]","text":"

    Lists all questions and their details

    Source code in src/pytanis/pretalx/client.py
    def questions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]:\n    \"\"\"Lists all questions and their details\"\"\"\n    return self._endpoint_lst(Question, event_slug, 'questions', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.review","title":"review(event_slug: str, id: int, *, params: QueryParams | None = None) -> Review","text":"

    Returns a specific review

    Source code in src/pytanis/pretalx/client.py
    def review(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Review:  # noqa: A002\n    \"\"\"Returns a specific review\"\"\"\n    return self._endpoint_id(Review, event_slug, 'reviews', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.reviews","title":"reviews(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]","text":"

    Lists all reviews and their details

    Source code in src/pytanis/pretalx/client.py
    def reviews(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]:\n    \"\"\"Lists all reviews and their details\"\"\"\n    return self._endpoint_lst(Review, event_slug, 'reviews', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.room","title":"room(event_slug: str, id: int, *, params: QueryParams | None = None) -> Room","text":"

    Returns a specific room

    Source code in src/pytanis/pretalx/client.py
    def room(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Room:  # noqa: A002\n    \"\"\"Returns a specific room\"\"\"\n    return self._endpoint_id(Room, event_slug, 'rooms', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.rooms","title":"rooms(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]","text":"

    Lists all rooms and their details

    Source code in src/pytanis/pretalx/client.py
    def rooms(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]:\n    \"\"\"Lists all rooms and their details\"\"\"\n    return self._endpoint_lst(Room, event_slug, 'rooms', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.info('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.speaker","title":"speaker(event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker","text":"

    Returns a specific speaker

    Source code in src/pytanis/pretalx/client.py
    def speaker(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker:\n    \"\"\"Returns a specific speaker\"\"\"\n    return self._endpoint_id(Speaker, event_slug, 'speakers', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.speakers","title":"speakers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]","text":"

    Lists all speakers and their details

    Source code in src/pytanis/pretalx/client.py
    def speakers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]:\n    \"\"\"Lists all speakers and their details\"\"\"\n    return self._endpoint_lst(Speaker, event_slug, 'speakers', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.submission","title":"submission(event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission","text":"

    Returns a specific submission

    Source code in src/pytanis/pretalx/client.py
    def submission(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission:\n    \"\"\"Returns a specific submission\"\"\"\n    return self._endpoint_id(Submission, event_slug, 'submissions', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.submissions","title":"submissions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]","text":"

    Lists all submissions and their details

    Source code in src/pytanis/pretalx/client.py
    def submissions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]:\n    \"\"\"Lists all submissions and their details\"\"\"\n    return self._endpoint_lst(Submission, event_slug, 'submissions', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.tag","title":"tag(event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag","text":"

    Returns a specific tag

    Source code in src/pytanis/pretalx/client.py
    def tag(self, event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag:\n    \"\"\"Returns a specific tag\"\"\"\n    return self._endpoint_id(Tag, event_slug, 'tags', tag, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.tags","title":"tags(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]","text":"

    Lists all tags and their details

    Source code in src/pytanis/pretalx/client.py
    def tags(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]:\n    \"\"\"Lists all tags and their details\"\"\"\n    return self._endpoint_lst(Tag, event_slug, 'tags', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.talk","title":"talk(event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk","text":"

    Returns a specific talk

    Source code in src/pytanis/pretalx/client.py
    def talk(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk:\n    \"\"\"Returns a specific talk\"\"\"\n    return self._endpoint_id(Talk, event_slug, 'talks', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.PretalxClient.talks","title":"talks(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]","text":"

    Lists all talks and their details

    Source code in src/pytanis/pretalx/client.py
    def talks(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]:\n    \"\"\"Lists all talks and their details\"\"\"\n    return self._endpoint_lst(Talk, event_slug, 'talks', params=params)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.reviews_as_df","title":"reviews_as_df(reviews: Iterable[Review]) -> pd.DataFrame","text":"

    Convert the reviews to a dataframe

    Source code in src/pytanis/pretalx/utils.py
    def reviews_as_df(reviews: Iterable[Review]) -> pd.DataFrame:\n    \"\"\"Convert the reviews to a dataframe\"\"\"\n    df = pd.DataFrame([review.model_dump() for review in reviews])\n    # make first letter of column upper-case in accordance with our convention\n    df.rename(columns={col: col.title() for col in df.columns}, inplace=True)\n    # user is the speaker name to use for joining\n    df.rename(columns={'User': Col.pretalx_user, 'Score': Col.review_score}, inplace=True)\n\n    return df\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.speakers_as_df","title":"speakers_as_df(speakers: Iterable[Speaker], *, with_questions: bool = False, question_prefix: str = 'Q: ') -> pd.DataFrame","text":"

    Convert speakers into a dataframe

    Make sure to have params={\"questions\": \"all\"} for the PretalxAPI if with_questions is True.

    Source code in src/pytanis/pretalx/utils.py
    def speakers_as_df(\n    speakers: Iterable[Speaker], *, with_questions: bool = False, question_prefix: str = 'Q: '\n) -> pd.DataFrame:\n    \"\"\"Convert speakers into a dataframe\n\n    Make sure to have `params={\"questions\": \"all\"}` for the PretalxAPI if `with_questions` is True.\n    \"\"\"\n    rows = []\n    for speaker in speakers:\n        row = {\n            Col.speaker_code: speaker.code,\n            Col.speaker_name: speaker.name,\n            Col.email: speaker.email,\n            Col.biography: speaker.biography,\n            Col.submission: speaker.submissions,\n        }\n        if with_questions and speaker.answers is not None:\n            for answer in speaker.answers:\n                # The API returns also questions that are 'per proposal/submission', we get these using the\n                # submission endpoint and don't want them here due to ambiguity if several submission were made.\n                if answer.person is not None:\n                    row[f'{question_prefix}{answer.question.question.en}'] = answer.answer\n        rows.append(row)\n    return pd.DataFrame(rows)\n
    "},{"location":"reference/pytanis/pretalx/#pytanis.pretalx.subs_as_df","title":"subs_as_df(subs: Iterable[Submission], *, with_questions: bool = False, question_prefix: str = 'Q: ') -> pd.DataFrame","text":"

    Convert submissions into a dataframe

    Make sure to have params={\"questions\": \"all\"} for the PretalxAPI if with_questions is True.

    Source code in src/pytanis/pretalx/utils.py
    def subs_as_df(\n    subs: Iterable[Submission], *, with_questions: bool = False, question_prefix: str = 'Q: '\n) -> pd.DataFrame:\n    \"\"\"Convert submissions into a dataframe\n\n    Make sure to have `params={\"questions\": \"all\"}` for the PretalxAPI if `with_questions` is True.\n    \"\"\"\n    rows = []\n    for sub in subs:\n        row = {\n            Col.submission: sub.code,\n            Col.title: sub.title,\n            Col.track: sub.track.en if sub.track else None,\n            Col.speaker_code: [speaker.code for speaker in sub.speakers],\n            Col.speaker_name: [speaker.name for speaker in sub.speakers],\n            Col.duration: sub.duration,\n            Col.submission_type: sub.submission_type.en,\n            Col.submission_type_id: sub.submission_type_id,\n            Col.state: sub.state.value,\n            Col.pending_state: None if sub.pending_state is None else sub.pending_state.value,\n            Col.created: sub.created,\n        }\n        if with_questions and sub.answers is not None:\n            for answer in sub.answers:\n                row[f'{question_prefix}{answer.question.question.en}'] = answer.answer\n        rows.append(row)\n    return pd.DataFrame(rows)\n
    "},{"location":"reference/pytanis/pretalx/client/","title":"Client","text":""},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client","title":"client","text":"

    Client for the Pretalx API

    Documentation: https://docs.pretalx.org/api/resources/index.html

    ToDo

    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.JSON","title":"JSON: TypeAlias = JSONObj | JSONLst module-attribute","text":"

    Type of the JSON response as returned by the Pretalx API

    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.JSONLst","title":"JSONLst: TypeAlias = list[JSONObj] module-attribute","text":"

    Type of a JSON list of JSON objects

    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.JSONObj","title":"JSONObj: TypeAlias = dict[str, Any] module-attribute","text":"

    Type of a JSON object (without recursion)

    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.T","title":"T = TypeVar('T', bound=BaseModel) module-attribute","text":""},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient","title":"PretalxClient(config: Config | None = None, *, blocking: bool = False)","text":"

    Client for the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def __init__(self, config: Config | None = None, *, blocking: bool = False):\n    if config is None:\n        config = get_cfg()\n    self._config = config\n    self._get_throttled = self._get\n    self.blocking = blocking\n    self.set_throttling(calls=2, seconds=1)  # we are nice by default and Pretalx doesn't allow many calls at once.\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.blocking","title":"blocking = blocking instance-attribute","text":""},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.answer","title":"answer(event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer","text":"

    Returns a specific answer

    Source code in src/pytanis/pretalx/client.py
    def answer(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Answer:  # noqa: A002\n    \"\"\"Returns a specific answer\"\"\"\n    return self._endpoint_id(Answer, event_slug, 'answers', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.answers","title":"answers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]","text":"

    Lists all answers and their details

    Source code in src/pytanis/pretalx/client.py
    def answers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Answer]]:\n    \"\"\"Lists all answers and their details\"\"\"\n    return self._endpoint_lst(Answer, event_slug, 'answers', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.event","title":"event(event_slug: str, *, params: QueryParams | None = None) -> Event","text":"

    Returns detailed information about a specific event

    Source code in src/pytanis/pretalx/client.py
    def event(self, event_slug: str, *, params: QueryParams | None = None) -> Event:\n    \"\"\"Returns detailed information about a specific event\"\"\"\n    endpoint = f'/api/events/{event_slug}/'\n    result = self._get_one(endpoint, params)\n    _logger.debug('result', resp=result)\n    return Event.model_validate(result)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.events","title":"events(*, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]","text":"

    Lists all events and their details

    Source code in src/pytanis/pretalx/client.py
    def events(self, *, params: QueryParams | None = None) -> tuple[int, Iterator[Event]]:\n    \"\"\"Lists all events and their details\"\"\"\n    count, results = self._get_many('/api/events/', params)\n    events = iter(_logger.debug('result', resp=r) or Event.model_validate(r) for r in results)\n    return count, events\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.me","title":"me() -> Me","text":"

    Returns what Pretalx knows about myself

    Source code in src/pytanis/pretalx/client.py
    def me(self) -> Me:\n    \"\"\"Returns what Pretalx knows about myself\"\"\"\n    result = self._get_one('/api/me')\n    return Me.model_validate(result)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.question","title":"question(event_slug: str, id: int, *, params: QueryParams | None = None) -> Question","text":"

    Returns a specific question

    Source code in src/pytanis/pretalx/client.py
    def question(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Question:  # noqa: A002\n    \"\"\"Returns a specific question\"\"\"\n    return self._endpoint_id(Question, event_slug, 'questions', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.questions","title":"questions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]","text":"

    Lists all questions and their details

    Source code in src/pytanis/pretalx/client.py
    def questions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Question]]:\n    \"\"\"Lists all questions and their details\"\"\"\n    return self._endpoint_lst(Question, event_slug, 'questions', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.review","title":"review(event_slug: str, id: int, *, params: QueryParams | None = None) -> Review","text":"

    Returns a specific review

    Source code in src/pytanis/pretalx/client.py
    def review(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Review:  # noqa: A002\n    \"\"\"Returns a specific review\"\"\"\n    return self._endpoint_id(Review, event_slug, 'reviews', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.reviews","title":"reviews(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]","text":"

    Lists all reviews and their details

    Source code in src/pytanis/pretalx/client.py
    def reviews(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Review]]:\n    \"\"\"Lists all reviews and their details\"\"\"\n    return self._endpoint_lst(Review, event_slug, 'reviews', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.room","title":"room(event_slug: str, id: int, *, params: QueryParams | None = None) -> Room","text":"

    Returns a specific room

    Source code in src/pytanis/pretalx/client.py
    def room(self, event_slug: str, id: int, *, params: QueryParams | None = None) -> Room:  # noqa: A002\n    \"\"\"Returns a specific room\"\"\"\n    return self._endpoint_id(Room, event_slug, 'rooms', id, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.rooms","title":"rooms(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]","text":"

    Lists all rooms and their details

    Source code in src/pytanis/pretalx/client.py
    def rooms(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Room]]:\n    \"\"\"Lists all rooms and their details\"\"\"\n    return self._endpoint_lst(Room, event_slug, 'rooms', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.set_throttling","title":"set_throttling(calls: int, seconds: int)","text":"

    Throttle the number of calls per seconds to the Pretalx API

    Source code in src/pytanis/pretalx/client.py
    def set_throttling(self, calls: int, seconds: int):\n    \"\"\"Throttle the number of calls per seconds to the Pretalx API\"\"\"\n    _logger.info('throttling', calls=calls, seconds=seconds)\n    self._get_throttled = throttle(calls, seconds)(self._get)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.speaker","title":"speaker(event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker","text":"

    Returns a specific speaker

    Source code in src/pytanis/pretalx/client.py
    def speaker(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Speaker:\n    \"\"\"Returns a specific speaker\"\"\"\n    return self._endpoint_id(Speaker, event_slug, 'speakers', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.speakers","title":"speakers(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]","text":"

    Lists all speakers and their details

    Source code in src/pytanis/pretalx/client.py
    def speakers(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Speaker]]:\n    \"\"\"Lists all speakers and their details\"\"\"\n    return self._endpoint_lst(Speaker, event_slug, 'speakers', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.submission","title":"submission(event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission","text":"

    Returns a specific submission

    Source code in src/pytanis/pretalx/client.py
    def submission(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Submission:\n    \"\"\"Returns a specific submission\"\"\"\n    return self._endpoint_id(Submission, event_slug, 'submissions', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.submissions","title":"submissions(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]","text":"

    Lists all submissions and their details

    Source code in src/pytanis/pretalx/client.py
    def submissions(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Submission]]:\n    \"\"\"Lists all submissions and their details\"\"\"\n    return self._endpoint_lst(Submission, event_slug, 'submissions', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.tag","title":"tag(event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag","text":"

    Returns a specific tag

    Source code in src/pytanis/pretalx/client.py
    def tag(self, event_slug: str, tag: str, *, params: QueryParams | None = None) -> Tag:\n    \"\"\"Returns a specific tag\"\"\"\n    return self._endpoint_id(Tag, event_slug, 'tags', tag, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.tags","title":"tags(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]","text":"

    Lists all tags and their details

    Source code in src/pytanis/pretalx/client.py
    def tags(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Tag]]:\n    \"\"\"Lists all tags and their details\"\"\"\n    return self._endpoint_lst(Tag, event_slug, 'tags', params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.talk","title":"talk(event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk","text":"

    Returns a specific talk

    Source code in src/pytanis/pretalx/client.py
    def talk(self, event_slug: str, code: str, *, params: QueryParams | None = None) -> Talk:\n    \"\"\"Returns a specific talk\"\"\"\n    return self._endpoint_id(Talk, event_slug, 'talks', code, params=params)\n
    "},{"location":"reference/pytanis/pretalx/client/#pytanis.pretalx.client.PretalxClient.talks","title":"talks(event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]","text":"

    Lists all talks and their details

    Source code in src/pytanis/pretalx/client.py
    def talks(self, event_slug: str, *, params: QueryParams | None = None) -> tuple[int, Iterator[Talk]]:\n    \"\"\"Lists all talks and their details\"\"\"\n    return self._endpoint_lst(Talk, event_slug, 'talks', params=params)\n
    "},{"location":"reference/pytanis/pretalx/types/","title":"Types","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types","title":"types","text":"

    Return types of the Pretalx API

    Documentation: https://docs.pretalx.org/api/resources/index.html

    Attention: Quite often the API docs and the actual results of the API differ!

    ToDo

    "},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer","title":"Answer","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.answer","title":"answer: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.answer_file","title":"answer_file: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.options","title":"options: list[Option] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.person","title":"person: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.question","title":"question: AnswerQuestionRef instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.review","title":"review: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Answer.submission","title":"submission: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.AnswerQuestionRef","title":"AnswerQuestionRef","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.AnswerQuestionRef.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.AnswerQuestionRef.question","title":"question: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event","title":"Event","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.date_from","title":"date_from: date instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.date_to","title":"date_to: date | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.is_public","title":"is_public: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.name","title":"name: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.slug","title":"slug: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.timezone","title":"timezone: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Event.urls","title":"urls: URLs instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Me","title":"Me","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Me.email","title":"email: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Me.local","title":"local: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Me.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Me.timezone","title":"timezone: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.MultiLingualStr","title":"MultiLingualStr","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.MultiLingualStr.de","title":"de: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.MultiLingualStr.en","title":"en: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Option","title":"Option","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Option.answer","title":"answer: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Option.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question","title":"Question","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.contains_personal_data","title":"contains_personal_data: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.deadline","title":"deadline: datetime | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.default_answer","title":"default_answer: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.freeze_after","title":"freeze_after: datetime | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.help_text","title":"help_text: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.is_public","title":"is_public: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.is_visible_to_reviewers","title":"is_visible_to_reviewers: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.max_length","title":"max_length: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.min_length","title":"min_length: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.options","title":"options: list[Option] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.question","title":"question: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.question_required","title":"question_required: QuestionRequirement instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.read_only","title":"read_only: bool | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.required","title":"required: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.target","title":"target: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Question.variant","title":"variant: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.QuestionRequirement","title":"QuestionRequirement","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.QuestionRequirement.after_deadline","title":"after_deadline = 'after deadline' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.QuestionRequirement.optional","title":"optional = 'optional' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.QuestionRequirement.required","title":"required = 'required' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Resource","title":"Resource","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Resource.description","title":"description: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Resource.resource","title":"resource: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review","title":"Review","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.answers","title":"answers: list[str] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.created","title":"created: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.score","title":"score: float | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.submission","title":"submission: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.text","title":"text: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.updated","title":"updated: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Review.user","title":"user: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room","title":"Room","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.availabilities","title":"availabilities: list[RoomAvailability] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.capacity","title":"capacity: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.description","title":"description: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.name","title":"name: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.position","title":"position: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Room.speaker_info","title":"speaker_info: MultiLingualStr | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.RoomAvailability","title":"RoomAvailability","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.RoomAvailability.end","title":"end: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.RoomAvailability.start","title":"start: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Slot","title":"Slot","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Slot.end","title":"end: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Slot.room","title":"room: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Slot.room_id","title":"room_id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Slot.start","title":"start: datetime instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Speaker","title":"Speaker","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Speaker.answers","title":"answers: list[Answer] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Speaker.availabilities","title":"availabilities: list[SpeakerAvailability] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Speaker.submissions","title":"submissions: list[str] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SpeakerAvailability","title":"SpeakerAvailability","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SpeakerAvailability.allDay","title":"allDay: str = Field(..., alias='all_day') class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SpeakerAvailability.end","title":"end: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SpeakerAvailability.id","title":"id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SpeakerAvailability.start","title":"start: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State","title":"State","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.accepted","title":"accepted = 'accepted' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.canceled","title":"canceled = 'canceled' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.confirmed","title":"confirmed = 'confirmed' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.deleted","title":"deleted = 'deleted' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.rejected","title":"rejected = 'rejected' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.submitted","title":"submitted = 'submitted' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.State.withdrawn","title":"withdrawn = 'withdrawn' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission","title":"Submission","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.abstract","title":"abstract: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.answers","title":"answers: list[Answer] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.code","title":"code: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.content_locale","title":"content_locale: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.created","title":"created: datetime | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.description","title":"description: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.do_not_record","title":"do_not_record: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.duration","title":"duration: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.image","title":"image: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.internal_notes","title":"internal_notes: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.is_featured","title":"is_featured: bool instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.notes","title":"notes: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.pending_state","title":"pending_state: State | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.resources","title":"resources: list[Resource] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.slot","title":"slot: Slot | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.slot_count","title":"slot_count: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.speakers","title":"speakers: list[SubmissionSpeaker] instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.state","title":"state: State instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.submission_type","title":"submission_type: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.submission_type_id","title":"submission_type_id: int instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.tag_ids","title":"tag_ids: list[int] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.tags","title":"tags: list[str] | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.title","title":"title: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.track","title":"track: MultiLingualStr | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Submission.track_id","title":"track_id: int | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker","title":"SubmissionSpeaker","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker.avatar","title":"avatar: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker.biography","title":"biography: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker.code","title":"code: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker.email","title":"email: str | None = None class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.SubmissionSpeaker.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Tag","title":"Tag","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Tag.color","title":"color: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Tag.description","title":"description: MultiLingualStr instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Tag.tag","title":"tag: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.Talk","title":"Talk","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.URLs","title":"URLs","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.URLs.base","title":"base: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.URLs.feed","title":"feed: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.URLs.login","title":"login: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.URLs.schedule","title":"schedule: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.User","title":"User","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.User.email","title":"email: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/types/#pytanis.pretalx.types.User.name","title":"name: str instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/","title":"Utils","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils","title":"utils","text":"

    Utilities related to Pretalx

    "},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col","title":"Col","text":"

    Convention of Pretalx column names for the functions below.

    "},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.affiliation","title":"affiliation = 'Affiliation' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.availability","title":"availability = 'Availability' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.availability_comment","title":"availability_comment = 'Availability Comment' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.biography","title":"biography = 'Biography' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.comment","title":"comment = 'Comment' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.created","title":"created = 'Created' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.duration","title":"duration = 'Duration' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.email","title":"email = 'Email' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.nreviews","title":"nreviews = '#Reviews' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.pending_state","title":"pending_state = 'Pending state' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.pretalx_user","title":"pretalx_user = 'Pretalx user' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.public","title":"public = 'Public' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.review_score","title":"review_score = 'Review Score' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.speaker_code","title":"speaker_code = 'Speaker code' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.speaker_name","title":"speaker_name = 'Speaker name' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.state","title":"state = 'State' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.submission","title":"submission = 'Submission' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.submission_type","title":"submission_type = 'Submission type' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.submission_type_id","title":"submission_type_id = 'Submission type id' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.title","title":"title = 'Title' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.Col.track","title":"track = 'Track' class-attribute instance-attribute","text":""},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.reviews_as_df","title":"reviews_as_df(reviews: Iterable[Review]) -> pd.DataFrame","text":"

    Convert the reviews to a dataframe

    Source code in src/pytanis/pretalx/utils.py
    def reviews_as_df(reviews: Iterable[Review]) -> pd.DataFrame:\n    \"\"\"Convert the reviews to a dataframe\"\"\"\n    df = pd.DataFrame([review.model_dump() for review in reviews])\n    # make first letter of column upper-case in accordance with our convention\n    df.rename(columns={col: col.title() for col in df.columns}, inplace=True)\n    # user is the speaker name to use for joining\n    df.rename(columns={'User': Col.pretalx_user, 'Score': Col.review_score}, inplace=True)\n\n    return df\n
    "},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.speakers_as_df","title":"speakers_as_df(speakers: Iterable[Speaker], *, with_questions: bool = False, question_prefix: str = 'Q: ') -> pd.DataFrame","text":"

    Convert speakers into a dataframe

    Make sure to have params={\"questions\": \"all\"} for the PretalxAPI if with_questions is True.

    Source code in src/pytanis/pretalx/utils.py
    def speakers_as_df(\n    speakers: Iterable[Speaker], *, with_questions: bool = False, question_prefix: str = 'Q: '\n) -> pd.DataFrame:\n    \"\"\"Convert speakers into a dataframe\n\n    Make sure to have `params={\"questions\": \"all\"}` for the PretalxAPI if `with_questions` is True.\n    \"\"\"\n    rows = []\n    for speaker in speakers:\n        row = {\n            Col.speaker_code: speaker.code,\n            Col.speaker_name: speaker.name,\n            Col.email: speaker.email,\n            Col.biography: speaker.biography,\n            Col.submission: speaker.submissions,\n        }\n        if with_questions and speaker.answers is not None:\n            for answer in speaker.answers:\n                # The API returns also questions that are 'per proposal/submission', we get these using the\n                # submission endpoint and don't want them here due to ambiguity if several submission were made.\n                if answer.person is not None:\n                    row[f'{question_prefix}{answer.question.question.en}'] = answer.answer\n        rows.append(row)\n    return pd.DataFrame(rows)\n
    "},{"location":"reference/pytanis/pretalx/utils/#pytanis.pretalx.utils.subs_as_df","title":"subs_as_df(subs: Iterable[Submission], *, with_questions: bool = False, question_prefix: str = 'Q: ') -> pd.DataFrame","text":"

    Convert submissions into a dataframe

    Make sure to have params={\"questions\": \"all\"} for the PretalxAPI if with_questions is True.

    Source code in src/pytanis/pretalx/utils.py
    def subs_as_df(\n    subs: Iterable[Submission], *, with_questions: bool = False, question_prefix: str = 'Q: '\n) -> pd.DataFrame:\n    \"\"\"Convert submissions into a dataframe\n\n    Make sure to have `params={\"questions\": \"all\"}` for the PretalxAPI if `with_questions` is True.\n    \"\"\"\n    rows = []\n    for sub in subs:\n        row = {\n            Col.submission: sub.code,\n            Col.title: sub.title,\n            Col.track: sub.track.en if sub.track else None,\n            Col.speaker_code: [speaker.code for speaker in sub.speakers],\n            Col.speaker_name: [speaker.name for speaker in sub.speakers],\n            Col.duration: sub.duration,\n            Col.submission_type: sub.submission_type.en,\n            Col.submission_type_id: sub.submission_type_id,\n            Col.state: sub.state.value,\n            Col.pending_state: None if sub.pending_state is None else sub.pending_state.value,\n            Col.created: sub.created,\n        }\n        if with_questions and sub.answers is not None:\n            for answer in sub.answers:\n                row[f'{question_prefix}{answer.question.question.en}'] = answer.answer\n        rows.append(row)\n    return pd.DataFrame(rows)\n
    "},{"location":"tasks/cfp/","title":"The Call for Participation Process","text":""},{"location":"tasks/cfp/#overview","title":"Overview","text":"

    The organisation of your conference programme starts with the Call for Participation/Proposals (CfP). Before you announce your CfP, obvious things to consider are:

    An example CfP can be found on the Call for Proposals of the PyConDE / PyData 2023 Berlin. The actual CfP submission is handled easily by Pretalx as you can just fill out everything in the web UI. As these tasks are all purely organisational/conceptional, there is no need to use Pytanis for any of this.

    "},{"location":"tasks/cfp/#example-milestones-of-pyconde-pydata-2023-berlin","title":"Example: Milestones of PyConDE / PyData 2023 Berlin","text":"

    Warning

    Obviously, you should never announce a CfP extension before the deadline of the original CfP is over \ud83d\ude1c. It is also useful to keep some of the above deadlines internally to avoid receiving a lot of emails when you have missed the deadline \ud83d\ude48.

    "},{"location":"tasks/review/","title":"The Review Process","text":""},{"location":"tasks/review/#overview","title":"Overview","text":"

    On a high-level, the review process of the proposals for a conference works as follows:

    1. find external reviewers and learn about their preferences,
    2. onboard reviewers in Pretalx,
    3. assign proposals to reviewers according to their preferences,
    4. communicate with the reviewers occasionally for updates,
    5. track the whole process.
    "},{"location":"tasks/review/#1-find-external-reviewers-and-learn-about-their-preferences","title":"1. Find External Reviewers and Learn about their Preferences","text":"

    For the PyConDE / PyData Berlin 2023, we were looking for about 50 external reviewers since we expected about 400 proposals, and we wanted to have 3 reviews per proposal. This would amount to about 25 proposals to review per person, which is manageable within a few weeks if you schedule 5-15 minutes per proposal.

    To get external reviewers, we decided that would only ask within our (Program Committee members') circle of trust and refer them to Google Forms. The form basically consisted of following questions with descriptions:

    Tip

    Every submission of the Google Form is then automatically added to a Google Sheet, let's call it the volunteer sheet, which can be easily read with the help of Pytanis. Check out our Google Sheet docs and Pytanis' google module to learn about more functionality.

    "},{"location":"tasks/review/#2-onboard-reviewers-in-pretalx","title":"2. Onboard Reviewers in Pretalx","text":"

    In Pretalx select Organisers in the left menu bar (you need Admin-rights for that) and click the teams under your event name. You should see a list of all teams and it's a good idea to have one for all reviewers, e.g. 2023-Reviewers-ALL. By clicking on the team name you get to a page that lists the names and corresponding e-mails of team members as well as an option to add new members at the bottom.

    You can now start typing in the e-mail addresses from the volunteer sheet to send out invitations to them. After volunteers accept the invitation they will show up with a user-name and e-mail in the team table. Now, here comes the tricky part that can cause a lot of confusion. If person A entered in the Google Form the e-mail address work@mail.com, and you added this in Pretalx, it might happen that person A accepts the team invitation with a different Pretalx account that is linked to the e-mail address private@mail.com. In this case, Pretalx will automatically replace work@mail.com, which was used for the invitation, with private@mail.com in the Pretalx table of team members. Unfortunately, Pretalx has no way of automatically tracking this change of mail addresses and this issue, as filed in #1417, is still unresolved.

    To work around this email issue and to be able to later join your volunteer sheet for instance with reviews, it makes sense to introduce a new column, e.g. \"Pretalx mail\", where you add the actual Pretalx account e-mail that was used by the invited user. Additionally, you should have a column for the Pretalx user-name, e.g. \"Pretalx user\", where you state the user-name by copying it over from the Pretalx team member table. This user-name column will be useful later to join our volunteer reviewers with the reviews they did, because the review-endpoint of Pretalx only returns the user-name, not the e-mail of a reviewer. This problem was also discussed in #1416 and is an intended behaviour.

    "},{"location":"tasks/review/#3-assign-proposals-to-reviewers-according-to-their-preferences","title":"3. Assign Proposals to Reviewers according to their Preferences","text":"

    Pretalx already provides a basic assignment feature so that proposals with the least number of reviews will show up earlier in the review queue so that they get more reviews. Additionally, Pretalx allows uploading a mapping JSON file so that you can assign certain proposals to a reviewer matching their preferences with the tracks of the proposals. Also, Pretalx is working on more elaborate automatic assignment features and some discussion about it can be found in issue #1331.

    Pytanis allows you to create JSON mapping files that can be uploaded in Pretalx under Review \u00bb Assign reviews. Then click Actions (upper right) \u00bb Import assignments and select the option Assign proposals to reviewers, choose the JSON file and make sure to always set Replace current assignments to Yes. Overwriting the current assignments makes sure that the assignment state in Pretalx is always consistent with what you expect. Also, be sure to always back up your assignment files somewhere in case you need to roll back later on. To make this easy, just name your files assignments-YYYYMMDD_I.json, where YYYY is the current year, MM the month, DD the day in the month and I the version increment, e.g. 1 or 2, in case you need several assignments throughout the same day.

    So how do you create an assignment file using Pytanis? Currently, we have implemented in a notebook an initial simple algorithm that can be easily run. Fancier algorithms will come in the future and don't hesitate to contribute. The main idea of the algorithm is to set a goal of number of reviews for each proposal, e.g. 3 reviews, and a certain buffer, e.g. 1. This means every proposal is assigned to goal number of reviews + buffer - current review number in case the current review number is not already equal or greater than the goal number of reviews. Rerunning this assignment frequently helps to avoid overshooting as the buffer mainly addresses the fact that you will also have inactive reviewers or some that start on the last day before your review deadline. For each proposal and remaining review, the algorithm assigns the proposals to:

    Be aware that some of your reviewers might have also make proposal submissions. Thus, it might happen by chance that someone gets assigned his/her own proposal using this approach but luckily Pretalx takes care of that--if the same Pretalx account was used.

    This quite simple algorithm can be found in the notebook 10_reviewer-assignment_v1. It uses Pytanis to pull the submission/proposals as well as the current reviews from Pretalx and joins them to get an overview of the current state of reviews. Then Pytanis is used to get the Google sheet of reviewers and their preferences, which is also joined with the data from Pretalx. Then the aforementioned algorithm is run and the assignment JSON file written.

    "},{"location":"tasks/review/#4-communicate-with-the-reviewers-occasionally-for-updates","title":"4. Communicate with the Reviewers occasionally for Updates","text":"

    From time to time, you want to get in contact with your reviewers to remind them of some deadline or just to say thank you for their work. Pytanis has an easy interface to HelpDesk that can be used as an e-mail client. For some practical examples, just check out the notebook 20_mail_to_reviewers_v1, the docs about mailing, as well as the Pytanis' mail references.

    "},{"location":"tasks/review/#5-track-the-whole-process","title":"5. Track the whole process","text":"

    During the review process it very important to keep track of review activity to make sure your internal deadlines for the review process are met. For instance, there might be reviewers that are having difficulties but have not reached out yet. So finding inactivate reviewers after a certain period of time and sending a nice supportive e-mail helps a lot. Also, some reviewers might have finished their batch of work early but might be up for more, thus identifying and getting in contact with them, is always a good idea. Many of those analyses are really individual, and you can check our examples in the notebook 10_reviewer-assignment_v1.

    "},{"location":"tasks/schedule/","title":"Creating the Schedule","text":"

    After all you talks and tutorials are confirmed, the next major milestone is to create a schedule so that each talk gets a time and place to be presented. Pretalx allows you to create a schedule by dragging & dropping the talk blocks onto a schedule, where you can define the number of days and rooms. You can also specify breaks like lunch or coffee breaks and later on publish the schedule for everyone. So this feature is pretty need but for larger conferences with a lot of parallel sessions, i.e. many rooms, some help might be needed.

    Assuming that you had some blank schedule before that already defines the time slots with their lengths and when the breaks are, then surely following constraints must be satisfied:

    Besides those constraints you might want to optimize for several objectives:

    1. the preferences for day and time of the speakers are considered (if they provided some),
    2. the more popular a talk is (from the public voting data), the more capacity the assigned room should have,
    3. if many people are highly interested in seeing two talks (voting data), these talks should rather not be scheduled in parallel. Also, sponsored talks should never be in parallel to avoid cannibalization,
    4. talks should have same main track, e.g. PyData, if they are in the same session (block of talks in one room),
    5. talks should have same sub track, e.g. PyData: Data Handling, if they are in the same session.

    The easiest way of dealing with multi-objective optimization is to create one new main objective by weighting and summing all objectives. For the objectives outlined above, it surely makes sense to choose the weights so that the importance is 1 > 2 > 3 > 4 > 5.

    In the notebook 50_scheduling_v1, you can find an example that uses Mixed-Integer-Programming (MIP) to generate a preliminary schedule that can be used as a starting point before creating the schedule in Pretalx. Although the constraints and objective from above may look quite simple, MIPs are not only hard, they are even NP-hard ;-) The example in the notebook uses Pyomo to formulate the problem and transform it into a standardized form, so that the solver HiGHS can do its job. In the concrete example, even after 24h no perfect solution was found, but the good thing is that the gap between best found feasible solution and the maximum possible objective value, i.e. the gap, was relatively small.

    Again, to visualize a solution like this, you can push it easily with the help of Pytanis to Google Sheets, which is illustrated in the figure below.

    Tip

    If you want to also specify link previews, sometimes also called a social banners, then check out the notebook 40_talk_image_v1 on how Pytanis can help you to create them.

    "},{"location":"tasks/selection/","title":"The Selection Process","text":""},{"location":"tasks/selection/#overview","title":"Overview","text":"

    On a high-level, the selection process involves the following

    1. have an optional public voting for the proposals,
    2. decide on how many talks, tutorials in which length, track or skill level you want to have,
    3. get an overview of the proposals, the speakers, the reviewer scores, and optionally the vote scores,
    4. select in Pretalx which talks are accepted and which ones are not.
    "},{"location":"tasks/selection/#1-optional-public-voting","title":"1. Optional Public Voting","text":"

    The pretalx-public-voting plugin allows to vote for the proposals which is a nice signal if a talk is generally interesting to the audience or not, solely based on the title and abstract. If it is installed activate it in Pretalx under Settings \u00bb Public voting. After the end date of the voting has passed this is also the place where you can download the results as a csv file. Unfortunately, there is currently no API provided by Pretalx for this feature.

    "},{"location":"tasks/selection/#2-decision-on-number-of-talks-and-rules-for-acceptance","title":"2. Decision on Number of Talks and Rules for Acceptance","text":"

    Deciding on the rules of acceptance might be one of the hardest parts and no Software can support you with it. It is really important to do this early on since it will help with the actual selection process. In order to decide for instance for the number of talks/tutorials in various lengths, it's important to already have a blank schedule, i.e. just the time slots, at hand. Diversity is also an important topic, so one rule might be to over-represent the under-represented but by how much? And do you expect your audience to be rather advanced, even senior, and what does that mean for ratio of the various required skill levels of the talks? How about the tracks you defined? Are speakers allowed to give more than one talk? How to deal with talks that have been given before? It's best to decide on a few guidelines before you proceed with the next steps.

    "},{"location":"tasks/selection/#3-overview-of-the-proposals","title":"3. Overview of the proposals","text":"

    Getting an overview of all proposals, their features, their review score and optionally their public score, is crucial when it comes to make a selection. Luckily with the help of Pytanis this is really easy. You can pull all the data from Pretalx, join it with additional data like the voting scores and push it to a Google Sheet, where everyone can easily view it and add comments. Find a practical example on how Pytanis was used for the PyConDE / PyData 2023 in this notebook 30_selection_v1.

    "},{"location":"tasks/selection/#4-final-selection-in-pretalx","title":"4. Final Selection in Pretalx","text":"

    Selecting the talks/tutorials for your conference is an iterative process. Maybe there are some talks you definitely want to select and others so bad you surely want to reject. Then there might be some you want to preliminarily accept or reject. Fortunately, Pretalx allows all that and Pytanis can pull that information to mark the rows in your GSheet with a certain colour. Here is an example on how this might look like.

    This example is also part of the notebook 30_selection_v1. Also be aware that after you accepted a talk or tutorial the author(s) must confirm. In practice, it happens also that accepted talks are withdrawn, so make sure you always keep a buffer of talks that haven't gotten any feedback yet to be able to accept some more.

    "},{"location":"usage/gsheet/","title":"Google Sheets","text":""},{"location":"usage/gsheet/#basic-usage","title":"Basic Usage","text":"

    Pytanis' Google Sheet client is really made for simplicity. Retrieving a worksheet of a Google sheet is as simple as:

    from pytanis import GSheetClient\n\ngsheet_client = GSheetClient()\ngsheet_df = gsheet_client.gsheet_as_df(SPREADSHEET_ID, WORKSHEET_NAME)\n
    where SPREADSHEET_ID is the ID taken from the spreadsheet's url, e.g. the ID is 17juVXM7V3p7Fgfi-9WkwPlMAYJB-DuxRhYCi_hastbB if your spreadsheet's url is https://docs.google.com/spreadsheets/d/17juVXM7V3p7Fgfi-9WkwPlMAYJB-DuxRhYCi_hastbB/edit#gid=1289752230, and WORKSHEET_NAME is the name of the actual sheet, e.g. Form responses 1, that you find in the lower bar of your spreadsheet. The function gsheet_as_df returns a simple Pandas dataframe, which most users are surely familiar with.

    If you run the above script the first time, you will get a link to a Google consent page, or it will directly open up if you run this in a Jupyter notebook. Read it carefully and accept the access to your Google Sheet. This step is only necessary and everytime you change the access scope. For instance, if you also want to have write-access to a worksheet, run:

    gsheet_client = GSheetClient(read_only=False)\ngsheet_client.recreate_token()\n
    and you will see the consent screen again, asking this time for write-access. Having accepted, you can now use
    gsheet_client.save_df_as_gsheet(subs_df, SPREADSHEET_ID, WORKSHEET_NAME)\n
    to upload a dataframe as Google sheet, overriding what's currently in there.

    Tip

    Google Sheet has a real useful version history that can be found under File \u00bb Version history \u00bb See version history. Even if you have accidentally overwritten you Google Sheet you can also restore an old version.

    "},{"location":"usage/gsheet/#advanced-usage","title":"Advanced Usage","text":"

    In case you want even more functionality and a dataframe is just not enough, you can use the gsheet method to get a Worksheet object or Spreadsheet object of GSpread. GSpread gives you full access to the API of Google Sheet and all the gsheet_as_df does is to basically use GSpread-Dataframe to convert this into a Pandas dataframe to simplify things for you. Also check out GSpread-Formatting if you want to use features like conditional formatting, colored cells, etc. Pytanis' google module gives you a complete reference of the current functionality within Pytanis but make sure to check out the GSpread ecosystem too as mentioned above.

    "},{"location":"usage/installation/","title":"Getting Started","text":""},{"location":"usage/installation/#installation","title":"Installation","text":"

    To install Pytanis simple run:

    pip install pytanis\n
    or to install all recommended additional dependencies:
    pip install 'pytanis[all]'\n
    Then create a configuration file and directory in your user's home directory. For Linux/MacOS/Unix use ~/.pytanis/config.toml and for Windows $HOME\\.pytanis\\config.toml, where $HOME is e.g. C:\\Users\\yourusername\\. Use your favourite editor to open config.toml within the .pytanis directory and add the following content:
    [Pretalx]\napi_token = \"932ndsf9uk32nf9sdkn3454532nj32jn\"\n\n[Google]\nclient_secret_json = \"client_secret.json\"\ntoken_json = \"token.json\"\n\n[HelpDesk]\naccount = \"934jcjkdf-39df-9df-93kf-934jfhuuij39fd\"\nentity_id = \"email@host.com\"\ntoken = \"dal:Sx4id934C3Y-X934jldjdfjk\"\n
    where you need to replace the dummy values in the sections [Pretalx] and [HelpDesk] accordingly.

    Info

    You have to configure the credentials and tokens only for the sections you actually want to use. For instance, [Pretalx] and [Google] are the most important sections for users that want to interact with Pretalx and also Google Sheets. If for instance no access to HelpDesk is necessary, e.g. no mails need to be sent, you can just leave out the key/value pairs in the [HelpDesk] section.

    "},{"location":"usage/installation/#retrieving-the-credentials-and-tokens","title":"Retrieving the Credentials and Tokens","text":""},{"location":"usage/mail/","title":"Sending Mails","text":""},{"location":"usage/mail/#basic-usage","title":"Basic Usage","text":"

    The usage of Pytanis' mail functionality is really simple. There are only three steps, you instantiate the mail client, create a mail object with your content and assemble a list of recipients.

    "},{"location":"usage/mail/#team-agent-id","title":"Team & Agent ID","text":"

    But before we write an e-mail we have to determine the team and agent id so that the e-mails we send are assigned to the right roles as set up within HelpDesk. In order to do this, we can just do:

    from pytanis import HelpDeskClient\n\nhelpdesk = HelpDeskClient()\n\nprint([agent.ID for agent in helpdesk.list_agents() if \"AGENTS EMAIL\" in agent.email])\nprint([team.ID for team in helpdesk.list_teams() if \"TEAM NAME\" in team.name])\n
    to find the right IDs with respect to the e-mail address AGENTS EMAIL and the corresponding TEAM NAME. We assume now that you stored those two values in agent_id and team_id, respectively.

    "},{"location":"usage/mail/#defining-the-recipients","title":"Defining the Recipients","text":"

    Defining the recipients means that you create a list of Recipient objects like:

    from pytanis.helpdesk import Recipient\n\nrecipients = [\n    Recipient(name=\"Peter Parker\", email=\"peter@parker.com\", address_as=\"Peter\"),\n    Recipient(name=\"Mary Watson\", email=\"marry-jane@watson.com\", address_as=\"Mary\"),\n]\n
    in most cases you will create this using a dataframe of some Google Sheet, and thus it will look more like:
    recipients = []\nrecip_df = google_sheet_df[[\"First name\", \"Last name\", \"E-mail\"]]\n\nfor _, row in recip_df.iterrows():\n    recipient = Recipient(\n        name=f\"{row['First name']} {row['Last name']}\",\n        email=row[\"E-mail\"],\n        address_as=row[\"First name\"],\n    )\n    recipients.append(recipient)\n

    For more advanced usages, e.g. individual mails corresponding to certain individuals, you can use the data parameter of the Recipient that takes a dictionary. Let's say we want to add a special sentence later for Peter to pay his rent, we can define:

    Recipient(\n    name=\"Peter Parker\",\n    email=\"peter@parker.com\",\n    address_as=\"Peter\",\n    data={\"feedback\": \"Pay your rent, Parker!\"},\n)\n
    In the section, we will see how we can access this special attribute again.

    "},{"location":"usage/mail/#writing-the-e-mail","title":"Writing the E-Mail","text":"

    So now we can write the actual e-mail text, which just uses the basic string substitution functionality of Python:

    mail_body = \"\"\"\nHi {recipient.address_as}!\n\nThis is a message from the Program committee with the subject {mail.subject} :-)\n{recipient.data.feedback}\n\nThank you very much {recipient.address_as} for your support!\n\nAll the best,\nProgram Committee\n\"\"\"\n
    You see that we can use recipient and mail to access the attributes of the Recipient as well as the Mail object to personalize the e-mail.

    Now we create the Mail object with:

    from pytanis.helpdesk import Mail\n\nmail = Mail(\n    subject=\"Deadline is coming soon\",\n    text=mail_body,\n    team_id=team_id,\n    agent_id=agent_id,\n    status=\"solved\",\n    recipients=recipients,\n)\n

    "},{"location":"usage/mail/#sending-an-e-mail","title":"Sending an E-mail","text":"

    Now we have everything assembled to send the e-mail with:

    from pytanis.helpdesk import MailClient\n\nmail_client = MailClient()\nresponses, errors = mail_client.send(mail, dry_run=True)\nassert not errors\n
    Having dry_run=True allows you to test you code and just print the resulting e-mails on your console to check if everything is like expected. Later set dry_run=False to actually send the e-mails via HelpDesk.

    The method send returns a list of successful responses and a hopefully empty list of errors. The responses list is a list of tuples where each tuple holds the Recipient as wells as the returned HelpDesk ticket. The errors list is a list of tuples with the Recipient and the corresponding exception object which occured when sending the mail to the recipient.

    "},{"location":"usage/mail/#advanced-usage","title":"Advanced Usage","text":"

    For more details, check out Pytanis' mail references and also the notebook 20_mail_to_reviewers_v1.

    Tip

    For contacting your (potential) speakers, Pretalx itself has pretty advanced templating and mailing features so there is no need to use this functionality here. Just make sure that you refer always to HelpDesk in your mails, so that you have a single point of managing mails and tickets.

    "},{"location":"usage/pretalx/","title":"Pretalx Client","text":""},{"location":"usage/pretalx/#basic-usage","title":"Basic Usage","text":"

    Pytanis offers easy access to the Pretalx API and the usage is quite self-explanatory. Let's look at some basic example:

    from pytanis import PretalxClient\n\nevent_name = \"pyconde-pydata-berlin-2023\"\n\npretalx_client = PretalxClient()\nsubs_count, subs = pretalx_client.submissions(event_name)\n
    This simple code will return the total number of submissions as subs_count and an iterator of all submissions subs. When iterating over subs new requests will be made internally to the Pretalx server to retrieve more result pages. This method of retrieving partial results is called pagination. Quite often you will just use subs = list(subs) to retrieve all submissions and get a list instead for easier handling. If you want to retrieve always all results directly, i.e. in a blocking way, you can tell this to the client via PretalxClient(blocking=True) but be aware that you must still call subs = list(subs).

    All endpoints of the Pretalx API are implemented in Pytanis and the method name corresponds to the name of the endpoint. Additional parameters can be passed using the params argument like e.g.:

    subs_count, subs = pretalx_client.submissions(\n    event_name, params={\"questions\": \"all\", \"state\": \"submitted\"}\n)\n
    Check the Pretalx API for a list of options.

    "},{"location":"usage/pretalx/#advanced-usage","title":"Advanced Usage","text":"

    Find out more about the client's capabilities, e.g. throttling, by looking at Pytanis' reference of the pretalx client module.

    "}]} \ No newline at end of file