diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..09c66b8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,15 @@ +* otf version: +* Python version: +* Operating System: + +### Description + +Describe what you were trying to get done. +Tell us what happened, what went wrong, and what you expected to happen. + +### What I Did + +``` +Paste the command(s) you ran and the output. +If there was a crash, please include the traceback here. +``` diff --git a/.github/dependabot.yml b/.github/dependabot.yml index add6757..50f7d64 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,4 +13,4 @@ updates: groups: python-packages: patterns: - - "*" + - "*" diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 0000000..d55c01a --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,161 @@ +name: dev build CI + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events + push: + branches: + - "*" + pull_request: + branches: + - "*" + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# contains 3 jobs: test, publish_dev_build and notification +jobs: + test: + # The type of runner that the job will run on + strategy: + matrix: + python-versions: ["3.10", "3.11"] + # github action doesn't goes well with windows due to docker support + # github action doesn't goes well with macos due to `no docker command` + #os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + # map step outputs to job outputs so they can be share among jobs + outputs: + package_version: ${{ steps.variables_step.outputs.package_version }} + package_name: ${{ steps.variables_step.outputs.package_name }} + repo_name: ${{ steps.variables_step.outputs.repo_name }} + repo_owner: ${{ steps.variables_step.outputs.repo_owner }} + build_number: ${{ steps.variables_step.outputs.build_number }} + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-versions }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions poetry + + # declare package_version, repo_owner, repo_name, package_name so you may use it in web hooks. + - name: Declare variables for convenient use + id: variables_step + run: | + echo "repo_owner=${GITHUB_REPOSITORY%/*}" >> $GITHUB_OUTPUT + echo "repo_name=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT + echo "package_name=`poetry version | awk '{print $1}'`" >> $GITHUB_OUTPUT + echo "package_version=`poetry version --short`" >> $GITHUB_OUTPUT + shell: bash + + - name: test with tox + run: tox + + # - uses: codecov/codecov-action@v3 + # with: + # fail_ci_if_error: true + + publish_dev_build: + # if test failed, we should not publish + needs: test + # you may need to change os below + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry tox tox-gh-actions + + - name: Build wheels and source tarball + run: | + poetry version $(poetry version --short)-dev.$GITHUB_RUN_NUMBER + poetry lock + poetry build + + # - name: publish to Test PyPI + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # user: __token__ + # password: ${{ secrets.TEST_PYPI_API_TOKEN}} + # repository_url: https://test.pypi.org/legacy/ + # skip_existing: true + + # notification: + # needs: [test,publish_dev_build] + # if: always() + # runs-on: ubuntu-latest + # steps: + # - uses: martialonline/workflow-status@v2 + # id: check + + # - name: build success notification via email + # if: ${{ steps.check.outputs.status == 'success' }} + # uses: dawidd6/action-send-mail@v3 + # with: + # server_address: ${{ secrets.BUILD_NOTIFY_MAIL_SERVER }} + # server_port: ${{ secrets.BUILD_NOTIFY_MAIL_PORT }} + # username: ${{ secrets.BUILD_NOTIFY_MAIL_FROM }} + # password: ${{ secrets.BUILD_NOTIFY_MAIL_PASSWORD }} + # from: build-bot + # to: ${{ secrets.BUILD_NOTIFY_MAIL_RCPT }} + # subject: ${{ needs.test.outputs.package_name }}.${{ needs.test.outputs.package_version}}.dev.${{ github.run_number }} build successfully + # convert_markdown: true + # html_body: | + # ## Build Success + # ${{ needs.test.outputs.package_name }}.${{ needs.test.outputs.package_version }}.dev.${{ github.run_number }} is built and published to test pypi + + # ## Change Details + # ${{ github.event.head_commit.message }} + + # For more information, please check change history at https://${{ needs.test.outputs.repo_owner }}.github.io/${{ needs.test.outputs.repo_name }}/${{ needs.test.outputs.package_version }}.dev/history + + # ## Package Download + # The pacakge is available at: https://test.pypi.org/project/${{ needs.test.outputs.package_name }}/ + + # - name: build failure notification via email + # if: ${{ steps.check.outputs.status == 'failure' }} + # uses: dawidd6/action-send-mail@v3 + # with: + # server_address: ${{ secrets.BUILD_NOTIFY_MAIL_SERVER }} + # server_port: ${{ secrets.BUILD_NOTIFY_MAIL_PORT }} + # username: ${{ secrets.BUILD_NOTIFY_MAIL_FROM }} + # password: ${{ secrets.BUILD_NOTIFY_MAIL_PASSWORD }} + # from: build-bot + # to: ${{ secrets.BUILD_NOTIFY_MAIL_RCPT }} + # subject: ${{ needs.test.outputs.package_name }}.${{ needs.test.outputs.package_version}}.dev.${{ github.run_number }} build failure + # convert_markdown: true + # html_body: | + # ## Change Details + # ${{ github.event.head_commit.message }} + + # ## View Log + # https://github.com/${{ needs.test.outputs.repo_owner }}/${{ needs.test.outputs.repo_name }}/actions + + # - name: Dingtalk Robot Notify + # if: always() + # uses: leafney/dingtalk-action@v1.0.0 + # env: + # DINGTALK_ACCESS_TOKEN: ${{ secrets.DINGTALK_ACCESS_TOKEN }} + # DINGTALK_SECRET: ${{ secrets.DINGTALK_SECRET }} + # with: + # msgtype: markdown + # title: CI Notification | Success + # text: | + # ### Build Success + # ${{ needs.test.outputs.package_version }}.dev.${{ github.run_number }}published to TEST pypi + # ### Change History + # Please check change history at https://${{ needs.test.outputs.repo_owner }}.github.io/${{ needs.test.outputs.repo_name }}/${{ needs.test.outputs.package_version }}.dev/history + # ### Package Download + # The pacakge is availabled at: https://test.pypi.org/project/${{ needs.test.outputs.repo_name }}/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7f6aea1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,163 @@ +# # Publish package on release branch if it's tagged with 'v*' + +# name: build & release + +# # Controls when the action will run. +# on: +# # Triggers the workflow on push or pull request events but only for the master branch +# push: +# branch: [main, master] +# tags: +# - 'v*' + +# # Allows you to run this workflow manually from the Actions tab +# workflow_dispatch: + +# # A workflow run is made up of one or more jobs that can run sequentially or in parallel +# jobs: +# release: +# runs-on: ubuntu-latest + +# strategy: +# matrix: +# python-versions: ['3.9'] + +# # map step outputs to job outputs so they can be share among jobs +# outputs: +# package_version: ${{ steps.variables_step.outputs.package_version }} +# package_name: ${{ steps.variables_step.outputs.package_name }} +# repo_name: ${{ steps.variables_step.outputs.repo_name }} +# repo_owner: ${{ steps.variables_step.outputs.repo_owner }} + +# # Steps represent a sequence of tasks that will be executed as part of the job +# steps: +# # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it +# - uses: actions/checkout@v4 + +# - name: build change log +# id: build_changelog +# uses: mikepenz/release-changelog-builder-action@v3.2.0 +# env: +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +# - uses: actions/setup-python@v4 +# with: +# python-version: ${{ matrix.python-versions }} + +# - name: Install dependencies +# run: | +# python -m pip install --upgrade pip +# pip install tox-gh-actions poetry + +# # declare package_version, repo_owner, repo_name, package_name so you may use it in web hooks. +# - name: Declare variables for convenient use +# id: variables_step +# run: | +# echo "repo_owner=${GITHUB_REPOSITORY%/*}" >> $GITHUB_OUTPUT +# echo "repo_name=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT +# echo "package_name=`poetry version | awk '{print $1}'`" >> $GITHUB_OUTPUT +# echo "package_version=`poetry version --short`" >> $GITHUB_OUTPUT +# shell: bash + +# - name: publish documentation +# run: | +# poetry install -E doc +# mkdocs build +# git config --global user.name Docs deploy +# git config --global user.email docs@dummy.bot.com +# mike deploy -p -f --ignore `poetry version --short` latest +# mike set-default -p `poetry version --short` + +# - name: Build wheels and source tarball +# run: | +# poetry lock +# poetry build + +# - name: Create Release +# id: create_release +# uses: actions/create-release@v1 +# env: +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +# with: +# tag_name: ${{ github.ref_name }} +# release_name: Release ${{ github.ref_name }} +# body: ${{ steps.build_changelog.outputs.changelog }} +# draft: false +# prerelease: false + +# - name: publish to PYPI +# uses: pypa/gh-action-pypi-publish@release/v1 +# with: +# user: __token__ +# password: ${{ secrets.PYPI_API_TOKEN }} +# skip_existing: true + +# notification: +# needs: release +# if: always() +# runs-on: ubuntu-latest +# steps: +# - uses: martialonline/workflow-status@v2 +# id: check + +# - name: build success notification via email +# if: ${{ steps.check.outputs.status == 'success' }} +# uses: dawidd6/action-send-mail@v3 +# with: +# server_address: ${{ secrets.BUILD_NOTIFY_MAIL_SERVER }} +# server_port: ${{ secrets.BUILD_NOTIFY_MAIL_PORT }} +# username: ${{ secrets.BUILD_NOTIFY_MAIL_FROM }} +# password: ${{ secrets.BUILD_NOTIFY_MAIL_PASSWORD }} +# from: build-bot +# to: ${{ secrets.BUILD_NOTIFY_MAIL_RCPT }} +# subject: ${{ needs.release.outputs.package_name }}.${{ needs.release.outputs.package_version}} build successfully +# convert_markdown: true +# html_body: | +# ## Build Success +# ${{ needs.release.outputs.package_name }}.${{ needs.release.outputs.package_version }} has been published to PYPI + +# ## Change Details +# ${{ github.event.head_commit.message }} + +# For more information, please check change history at https://${{ needs.release.outputs.repo_owner }}.github.io/${{ needs.release.outputs.repo_name }}/${{ needs.release.outputs.package_version }}/history + +# ## Package Download +# The pacakge is available at: https://pypi.org/project/${{ needs.release.outputs.package_name }}/ + +# - name: build failure notification via email +# if: ${{ steps.check.outputs.status == 'failure' }} +# uses: dawidd6/action-send-mail@v3 +# with: +# server_address: ${{ secrets.BUILD_NOTIFY_MAIL_SERVER }} +# server_port: ${{ secrets.BUILD_NOTIFY_MAIL_PORT }} +# username: ${{ secrets.BUILD_NOTIFY_MAIL_FROM }} +# password: ${{ secrets.BUILD_NOTIFY_MAIL_PASSWORD }} +# from: build-bot +# to: ${{ secrets.BUILD_NOTIFY_MAIL_RCPT }} +# subject: ${{ needs.release.outputs.package_name }}.${{ needs.release.outputs.package_version}} build failure +# convert_markdown: true +# html_body: | +# ## Change Details +# ${{ github.event.head_commit.message }} + +# ## Status: ${{ steps.check.outputs.status }} + +# ## View Log +# https://github.com/${{ needs.release.outputs.repo_owner }}/${{ needs.release.outputs.repo_name }}/actions + +# # - name: Dingtalk Robot Notify +# # if: always() +# # uses: leafney/dingtalk-action@v1.0.0 +# # env: +# # DINGTALK_ACCESS_TOKEN: ${{ secrets.DINGTALK_ACCESS_TOKEN }} +# # DINGTALK_SECRET: ${{ secrets.DINGTALK_SECRET }} +# # with: +# # msgtype: markdown +# # title: CI Notification | Success +# # text: | +# # ### ${{ needs.release.outputs.package_name }} Build Success +# # ${{ needs.release.outputs.package_version }} has been published to PYPI +# # ### Change History +# # Please check change history at https://${{ needs.release.outputs.repo_owner }}.github.io/${{ needs.release.outputs.repo_name }}/latest/history +# # ### Package Download +# # Please download the pacakge at: https://pypi.org/project/${{ needs.release.outputs.repo_name }}/ diff --git a/docs/reference/SUMMARY.md b/docs/reference/SUMMARY.md deleted file mode 100644 index c02c15f..0000000 --- a/docs/reference/SUMMARY.md +++ /dev/null @@ -1,31 +0,0 @@ -* [otf_api](otf_api/index.md) - * [api](otf_api/api.md) - * [classes_api](otf_api/classes_api.md) - * [dna_api](otf_api/dna_api.md) - * [member_api](otf_api/member_api.md) - * [models](otf_api/models/index.md) - * [auth](otf_api/models/auth.md) - * [base](otf_api/models/base.md) - * [responses](otf_api/models/responses/index.md) - * [bookings](otf_api/models/responses/bookings.md) - * [challenge_tracker_content](otf_api/models/responses/challenge_tracker_content.md) - * [challenge_tracker_detail](otf_api/models/responses/challenge_tracker_detail.md) - * [classes](otf_api/models/responses/classes.md) - * [dna_hr_history](otf_api/models/responses/dna_hr_history.md) - * [dna_max_hr](otf_api/models/responses/dna_max_hr.md) - * [dna_telemetry](otf_api/models/responses/dna_telemetry.md) - * [enums](otf_api/models/responses/enums.md) - * [favorite_studios](otf_api/models/responses/favorite_studios.md) - * [latest_agreement](otf_api/models/responses/latest_agreement.md) - * [member_detail](otf_api/models/responses/member_detail.md) - * [member_membership](otf_api/models/responses/member_membership.md) - * [member_purchases](otf_api/models/responses/member_purchases.md) - * [out_of_studio_workout_history](otf_api/models/responses/out_of_studio_workout_history.md) - * [performance_summary_detail](otf_api/models/responses/performance_summary_detail.md) - * [performance_summary_list](otf_api/models/responses/performance_summary_list.md) - * [studio_detail](otf_api/models/responses/studio_detail.md) - * [studio_services](otf_api/models/responses/studio_services.md) - * [total_classes](otf_api/models/responses/total_classes.md) - * [workouts](otf_api/models/responses/workouts.md) - * [performance_api](otf_api/performance_api.md) - * [studio_api](otf_api/studio_api.md) diff --git a/docs/reference/otf_api/api.md b/docs/reference/otf_api/api.md deleted file mode 100644 index 28eadbe..0000000 --- a/docs/reference/otf_api/api.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.api diff --git a/docs/reference/otf_api/classes_api.md b/docs/reference/otf_api/classes_api.md deleted file mode 100644 index dd722db..0000000 --- a/docs/reference/otf_api/classes_api.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.classes_api diff --git a/docs/reference/otf_api/dna_api.md b/docs/reference/otf_api/dna_api.md deleted file mode 100644 index 91e6a05..0000000 --- a/docs/reference/otf_api/dna_api.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.dna_api diff --git a/docs/reference/otf_api/index.md b/docs/reference/otf_api/index.md deleted file mode 100644 index b37b8ab..0000000 --- a/docs/reference/otf_api/index.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api diff --git a/docs/reference/otf_api/member_api.md b/docs/reference/otf_api/member_api.md deleted file mode 100644 index 5700b1e..0000000 --- a/docs/reference/otf_api/member_api.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.member_api diff --git a/docs/reference/otf_api/models/auth.md b/docs/reference/otf_api/models/auth.md deleted file mode 100644 index 248cb02..0000000 --- a/docs/reference/otf_api/models/auth.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.auth diff --git a/docs/reference/otf_api/models/base.md b/docs/reference/otf_api/models/base.md deleted file mode 100644 index 7e1f338..0000000 --- a/docs/reference/otf_api/models/base.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.base diff --git a/docs/reference/otf_api/models/index.md b/docs/reference/otf_api/models/index.md deleted file mode 100644 index 4d28c70..0000000 --- a/docs/reference/otf_api/models/index.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models diff --git a/docs/reference/otf_api/models/responses/bookings.md b/docs/reference/otf_api/models/responses/bookings.md deleted file mode 100644 index 8f5fece..0000000 --- a/docs/reference/otf_api/models/responses/bookings.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.bookings diff --git a/docs/reference/otf_api/models/responses/challenge_tracker_content.md b/docs/reference/otf_api/models/responses/challenge_tracker_content.md deleted file mode 100644 index bce7639..0000000 --- a/docs/reference/otf_api/models/responses/challenge_tracker_content.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.challenge_tracker_content diff --git a/docs/reference/otf_api/models/responses/challenge_tracker_detail.md b/docs/reference/otf_api/models/responses/challenge_tracker_detail.md deleted file mode 100644 index 5f171db..0000000 --- a/docs/reference/otf_api/models/responses/challenge_tracker_detail.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.challenge_tracker_detail diff --git a/docs/reference/otf_api/models/responses/classes.md b/docs/reference/otf_api/models/responses/classes.md deleted file mode 100644 index c4538e2..0000000 --- a/docs/reference/otf_api/models/responses/classes.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.classes diff --git a/docs/reference/otf_api/models/responses/dna_hr_history.md b/docs/reference/otf_api/models/responses/dna_hr_history.md deleted file mode 100644 index af2ebd7..0000000 --- a/docs/reference/otf_api/models/responses/dna_hr_history.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.dna_hr_history diff --git a/docs/reference/otf_api/models/responses/dna_max_hr.md b/docs/reference/otf_api/models/responses/dna_max_hr.md deleted file mode 100644 index 82df55e..0000000 --- a/docs/reference/otf_api/models/responses/dna_max_hr.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.dna_max_hr diff --git a/docs/reference/otf_api/models/responses/dna_telemetry.md b/docs/reference/otf_api/models/responses/dna_telemetry.md deleted file mode 100644 index 3ff5415..0000000 --- a/docs/reference/otf_api/models/responses/dna_telemetry.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.dna_telemetry diff --git a/docs/reference/otf_api/models/responses/enums.md b/docs/reference/otf_api/models/responses/enums.md deleted file mode 100644 index c555789..0000000 --- a/docs/reference/otf_api/models/responses/enums.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.enums diff --git a/docs/reference/otf_api/models/responses/favorite_studios.md b/docs/reference/otf_api/models/responses/favorite_studios.md deleted file mode 100644 index 1440568..0000000 --- a/docs/reference/otf_api/models/responses/favorite_studios.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.favorite_studios diff --git a/docs/reference/otf_api/models/responses/index.md b/docs/reference/otf_api/models/responses/index.md deleted file mode 100644 index d9c005d..0000000 --- a/docs/reference/otf_api/models/responses/index.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses diff --git a/docs/reference/otf_api/models/responses/latest_agreement.md b/docs/reference/otf_api/models/responses/latest_agreement.md deleted file mode 100644 index 4166238..0000000 --- a/docs/reference/otf_api/models/responses/latest_agreement.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.latest_agreement diff --git a/docs/reference/otf_api/models/responses/member_detail.md b/docs/reference/otf_api/models/responses/member_detail.md deleted file mode 100644 index befff5d..0000000 --- a/docs/reference/otf_api/models/responses/member_detail.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.member_detail diff --git a/docs/reference/otf_api/models/responses/member_membership.md b/docs/reference/otf_api/models/responses/member_membership.md deleted file mode 100644 index 66b9fb6..0000000 --- a/docs/reference/otf_api/models/responses/member_membership.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.member_membership diff --git a/docs/reference/otf_api/models/responses/member_purchases.md b/docs/reference/otf_api/models/responses/member_purchases.md deleted file mode 100644 index f516f23..0000000 --- a/docs/reference/otf_api/models/responses/member_purchases.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.member_purchases diff --git a/docs/reference/otf_api/models/responses/out_of_studio_workout_history.md b/docs/reference/otf_api/models/responses/out_of_studio_workout_history.md deleted file mode 100644 index 89f25e3..0000000 --- a/docs/reference/otf_api/models/responses/out_of_studio_workout_history.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.out_of_studio_workout_history diff --git a/docs/reference/otf_api/models/responses/performance_summary_detail.md b/docs/reference/otf_api/models/responses/performance_summary_detail.md deleted file mode 100644 index 3b7b3d2..0000000 --- a/docs/reference/otf_api/models/responses/performance_summary_detail.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.performance_summary_detail diff --git a/docs/reference/otf_api/models/responses/performance_summary_list.md b/docs/reference/otf_api/models/responses/performance_summary_list.md deleted file mode 100644 index be919ed..0000000 --- a/docs/reference/otf_api/models/responses/performance_summary_list.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.performance_summary_list diff --git a/docs/reference/otf_api/models/responses/studio_detail.md b/docs/reference/otf_api/models/responses/studio_detail.md deleted file mode 100644 index af7a07d..0000000 --- a/docs/reference/otf_api/models/responses/studio_detail.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.studio_detail diff --git a/docs/reference/otf_api/models/responses/studio_services.md b/docs/reference/otf_api/models/responses/studio_services.md deleted file mode 100644 index b133f5c..0000000 --- a/docs/reference/otf_api/models/responses/studio_services.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.studio_services diff --git a/docs/reference/otf_api/models/responses/total_classes.md b/docs/reference/otf_api/models/responses/total_classes.md deleted file mode 100644 index e3306be..0000000 --- a/docs/reference/otf_api/models/responses/total_classes.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.total_classes diff --git a/docs/reference/otf_api/models/responses/workouts.md b/docs/reference/otf_api/models/responses/workouts.md deleted file mode 100644 index 29b5543..0000000 --- a/docs/reference/otf_api/models/responses/workouts.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.models.responses.workouts diff --git a/docs/reference/otf_api/performance_api.md b/docs/reference/otf_api/performance_api.md deleted file mode 100644 index e3230c1..0000000 --- a/docs/reference/otf_api/performance_api.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.performance_api diff --git a/docs/reference/otf_api/studio_api.md b/docs/reference/otf_api/studio_api.md deleted file mode 100644 index a10d858..0000000 --- a/docs/reference/otf_api/studio_api.md +++ /dev/null @@ -1 +0,0 @@ -::: otf_api.studio_api diff --git a/docs/usage.md b/docs/usage.md index 82a3f4a..0a97b44 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -7,10 +7,10 @@ To use the API, you need to create an instance of the `Api` class, providing you The `Api` object has multiple api objects as attributes, which you can use to make requests to the API. The available api objects are: - `classes_api` -- `dna_api` (workout telemetry) - `members_api` - `performance_api` - `studios_api` +- `telemetry_api` Each of these api objects has methods that correspond to the endpoints in the API. You can use these methods to make requests to the API and get the data you need. @@ -418,7 +418,7 @@ async def main(): # telemetry is a detailed record of a specific workout - minute by minute, or more granular if desired # this endpoint takes a class_history_uuid, as well as a number of max data points - if you do not pass # this value it will attempt to return enough data points for 30 second intervals - telemetry = await otf.dna_api.get_telemetry(workouts.workouts[0].class_history_uuid) + telemetry = await otf.telemetry_api.get_telemetry(workouts.workouts[0].class_history_uuid) print(json.dumps(telemetry.model_dump(), indent=4, default=str)) """ diff --git a/examples/workout_examples.py b/examples/workout_examples.py index 36bed89..a92459c 100644 --- a/examples/workout_examples.py +++ b/examples/workout_examples.py @@ -155,7 +155,7 @@ async def main(): # telemetry is a detailed record of a specific workout - minute by minute, or more granular if desired # this endpoint takes a class_history_uuid, as well as a number of max data points - if you do not pass # this value it will attempt to return enough data points for 30 second intervals - telemetry = await otf.dna_api.get_telemetry(workouts.workouts[0].class_history_uuid) + telemetry = await otf.telemetry_api.get_telemetry(workouts.workouts[0].class_history_uuid) print(json.dumps(telemetry.model_dump(), indent=4, default=str)) """ diff --git a/mkdocs.yml b/mkdocs.yml index 6fcaf70..5084407 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,6 +24,7 @@ theme: icon: material/weather-sunny name: Switch to light mode features: + - navigation.index - navigation.tabs - navigation.instant - navigation.tabs.sticky @@ -66,6 +67,14 @@ plugins: handlers: python: options: + merge_init_into_class: true + show_root_toc_entry: false + show_symbol_type_toc: true + filters: + - "!^_" + - "^__" + members_order: source + signature_crossrefs: true extensions: - griffe_fieldz: { include_inherited: true } show_source: true @@ -81,3 +90,7 @@ extra: name: Github - icon: material/email link: "mailto:j.smith.git1@gmail.com" + +watch: + - src/otf_api + - scripts/gen_ref_pages.py diff --git a/poetry.lock b/poetry.lock index d704c8b..adeb4fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -194,6 +194,52 @@ files = [ docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] +[[package]] +name = "black" +version = "24.4.2" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "boto3" version = "1.34.125" @@ -2689,4 +2735,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "8eaf896e0776a5d70868749b8ba41724f7477825a62d20b7d3baaf899d473649" +content-hash = "bcbcc13ead6615bd50c70bae9c0950fdf1e0ada4dc6b388e23a3c08888a46d0a" diff --git a/pyproject.toml b/pyproject.toml index 64f349f..2710104 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ ruff = "0.4.8" pre-commit = "3.7.1" mypy = "1.10.0" twine = "5.1.0" +black = "^24.4.2" [tool.poetry.group.docs.dependencies] diff --git a/scripts/gen_ref_pages.py b/scripts/gen_ref_pages.py index 4e356a8..fdf4faa 100644 --- a/scripts/gen_ref_pages.py +++ b/scripts/gen_ref_pages.py @@ -1,5 +1,4 @@ -"""Generate the code reference pages and navigation.""" - +import shutil from pathlib import Path import mkdocs_gen_files @@ -9,6 +8,10 @@ root = Path(__file__).parent.parent src = root / "src" +REF_DIR = root / "docs" / "reference" +if REF_DIR.exists(): + shutil.rmtree(REF_DIR) + for path in sorted(src.rglob("*.py")): module_path = path.relative_to(src).with_suffix("") doc_path = path.relative_to(src).with_suffix(".md") @@ -23,7 +26,18 @@ elif parts[-1] == "__main__": continue - nav[parts] = doc_path.as_posix() + title_parts = [] + for part in parts: + sub_parts = part.split("_") + for i, sub_part in enumerate(sub_parts): + if sub_part in ["api", "hr"]: + sub_parts[i] = sub_part.upper() + else: + sub_parts[i] = sub_part.capitalize() + part = " ".join(sub_parts) + title_parts.append(part) + + nav[title_parts] = doc_path.as_posix() with mkdocs_gen_files.open(full_doc_path, "w") as fd: ident = ".".join(parts) diff --git a/src/otf_api/__init__.py b/src/otf_api/__init__.py index 4b58a56..642b9fd 100644 --- a/src/otf_api/__init__.py +++ b/src/otf_api/__init__.py @@ -1,4 +1,4 @@ -from . import classes_api, dna_api, member_api, studio_api +from . import classes_api, member_api, studios_api, telemetry_api from .api import Api from .models.auth import User from .models.responses import ( @@ -10,9 +10,6 @@ ChallengeTrackerContent, ChallengeTrackerDetailList, ChallengeType, - DnaHrHistory, - DnaMaxHr, - DnaTelemetry, EquipmentType, FavoriteStudioList, HistoryClassStatus, @@ -28,6 +25,9 @@ StudioDetailList, StudioServiceList, StudioStatus, + TelemetryHrHistory, + TelemetryItem, + TelemetryMaxHr, TotalClasses, WorkoutList, ) @@ -55,11 +55,11 @@ "FavoriteStudioList", "OtfClassList", "classes_api", - "studio_api", - "dna_api", - "DnaHrHistory", - "DnaTelemetry", - "DnaMaxHr", + "studios_api", + "telemetry_api", + "TelemetryHrHistory", + "TelemetryItem", + "TelemetryMaxHr", "StudioDetail", "StudioDetailList", "ALL_CLASS_STATUS", diff --git a/src/otf_api/api.py b/src/otf_api/api.py index f4676ab..b0372ee 100644 --- a/src/otf_api/api.py +++ b/src/otf_api/api.py @@ -7,11 +7,11 @@ from yarl import URL from otf_api.classes_api import ClassesApi -from otf_api.dna_api import DnaApi from otf_api.member_api import MemberApi from otf_api.models.auth import User from otf_api.performance_api import PerformanceApi -from otf_api.studio_api import StudiosApi +from otf_api.studios_api import StudiosApi +from otf_api.telemetry_api import TelemtryApi if typing.TYPE_CHECKING: from loguru import Logger @@ -21,23 +21,33 @@ API_BASE_URL = "api.orangetheory.co" API_IO_BASE_URL = "api.orangetheory.io" -API_DNA_BASE_URL = "api.yuzu.orangetheory.com" +API_TELEMETRY_BASE_URL = "api.yuzu.orangetheory.com" REQUEST_HEADERS = {"Authorization": None, "Content-Type": "application/json", "Accept": "application/json"} class Api: + """The main class of the otf-api library. Create an instance using the async method `create`. + + Example: + --- + ```python + import asyncio + from otf_api import Api + + async def main(): + otf = await Api.create("username", "password") + print(otf.member) + + if __name__ == "__main__": + asyncio.run(main()) + ``` + """ + logger: "Logger" = logger user: User session: aiohttp.ClientSession def __init__(self, username: str, password: str): - """Create a new API instance. The username and password are required arguments because even though - we cache the token, they expire so quickly that we usually end up needing to re-authenticate. - - Args: - username (str): The username of the user. - password (str): The password of the user. - """ self.member: MemberDetail self.home_studio: StudioDetail @@ -47,7 +57,7 @@ def __init__(self, username: str, password: str): self.member_api = MemberApi(self) self.classes_api = ClassesApi(self) self.studios_api = StudiosApi(self) - self.dna_api = DnaApi(self) + self.telemetry_api = TelemtryApi(self) self.performance_api = PerformanceApi(self) @classmethod @@ -77,7 +87,7 @@ async def _close_session(self) -> None: await self.session.close() @property - def base_headers(self) -> dict[str, str]: + def _base_headers(self) -> dict[str, str]: """Get the base headers for the API.""" if not self.user: raise ValueError("No user is logged in.") @@ -106,9 +116,9 @@ async def _do( logger.debug(f"Making {method!r} request to {full_url}, params: {params}") if headers: - headers.update(self.base_headers) + headers.update(self._base_headers) else: - headers = self.base_headers + headers = self._base_headers async with self.session.request(method, full_url, headers=headers, params=params) as response: response.raise_for_status() @@ -122,9 +132,9 @@ async def _default_request(self, method: str, url: str, params: dict[str, Any] | """Perform an API request to the default API.""" return await self._do(method, API_BASE_URL, url, params) - async def _dna_request(self, method: str, url: str, params: dict[str, Any] | None = None) -> Any: - """Perform an API request to the DNA API.""" - return await self._do(method, API_DNA_BASE_URL, url, params) + async def _telemetry_request(self, method: str, url: str, params: dict[str, Any] | None = None) -> Any: + """Perform an API request to the Telemetry API.""" + return await self._do(method, API_TELEMETRY_BASE_URL, url, params) async def _performance_summary_request( self, method: str, url: str, headers: dict[str, str], params: dict[str, Any] | None = None diff --git a/src/otf_api/member_api.py b/src/otf_api/member_api.py index fb55b9f..2e3b21d 100644 --- a/src/otf_api/member_api.py +++ b/src/otf_api/member_api.py @@ -36,12 +36,14 @@ def __init__(self, api: "Api"): async def get_workouts(self) -> WorkoutList: """Get the list of workouts from OT Live. - This returns data from the same api the OT Live website uses. It is quite a bit of data, - and all workouts going back to ~2019. The data includes the class history UUID, which can be used to get - telemetry data for a specific workout. - Returns: WorkoutList: The list of workouts. + + Info: + --- + This returns data from the same api the [OT Live website](https://otlive.orangetheory.com/) uses. + It is quite a bit of data, and all workouts going back to ~2019. The data includes the class history + UUID, which can be used to get telemetry data for a specific workout. """ res = await self._api._default_request("GET", "/virtual-class/in-studio-workouts") @@ -72,31 +74,32 @@ async def get_bookings( status (BookingStatus | None): The status of the bookings to get. Default is None, which includes\ all statuses. Only a single status can be provided. - Note: Looking at the code in the app, it appears that this endpoint accepts multiple statuses. Indeed, - it does not throw an error if you include a list of statuses. However, only the last status in the list is - used. I'm not sure if this is a bug or if the API is supposed to work this way. - - Note: This endpoint does not seem to provide much in the way of historical data. As far as I can tell, - it can go back about a month, potentially all of the data for the current month and prior month, as well as - as forward as they have classes scheduled (usually 30 days from today's date). Looking at the app code, - I believe the performance summaries endpoint is used for historical data. - - Potentially 45 days back and 30 days forward, if dates are provided. If no dates are provided, seems to - default to current month? - - Note: CheckedIn does not seem to return anything unless you provide dates. - - Note: Incorrect/invalid statuses do not cause any bad status code, they just return no results. - - Filtering best guesses: - - If status == Cancelled, the results are filtered class dates. If no dates provided, - returns those cancelled on current date? - - If status == Booked, the results are filtered on the class dates. - - If status == CheckedIn, the results are filtered on the class dates. - - If status == Waitlist, the results are filtered on class dates. - Returns: BookingList: The member's bookings. + + Warning: + --- + Incorrect statuses do not cause any bad status code, they just return no results. + + Tip: + --- + `CheckedIn` - you must provide dates if you want to get bookings with a status of CheckedIn. If you do not + provide dates, the endpoint will return no results for this status. + + Dates Notes: + --- + If dates are provided, the endpoint will return bookings where the class date is within the provided + date range. If no dates are provided, it seems to default to the current month. + + In general, this endpoint does not seem to be able to access bookings older than a certain point. It seems + to be able to go back about 45 days or a month. For current/future dates, it seems to be able to go forward + to as far as you can book classes in the app, which is usually 30 days from today's date. + + Developer Notes: + --- + Looking at the code in the app, it appears that this endpoint accepts multiple statuses. Indeed, + it does not throw an error if you include a list of statuses. However, only the last status in the list is + used. I'm not sure if this is a bug or if the API is supposed to work this way. """ if isinstance(start_date, date): @@ -120,25 +123,28 @@ async def _get_bookings_old(self, status: BookingStatus | None = None) -> Bookin status (BookingStatus | None): The status of the bookings to get. Default is None, which includes all statuses. Only a single status can be provided. - Note: This one is called with the param named 'status'. Dates cannot be provided, because if the endpoint - receives a date, it will return as if the param name was 'statuses'. - - Note: This seems to only work for Cancelled, Booked, CheckedIn, and Waitlisted statuses. If you provide - a different status, it will return all bookings, not filtered by status. The results in this scenario do - not line up with the `get_bookings` with no status provided, as that returns fewer records. Likely the - filtered dates are different on the backend. - - My guess: the endpoint called with dates and 'statuses' is a "v2" kind of thing, where they upgraded without - changing the version of the api. Calling it with no dates and a singular (limited) status is probably v1. - - I'm leaving this in here for reference, but marking it private. I just don't want to have to puzzle over this - again if I remove it and forget about it. - Returns: BookingList: The member's bookings. Raises: ValueError: If an unaccepted status is provided. + + Notes: + --- + This one is called with the param named 'status'. Dates cannot be provided, because if the endpoint + receives a date, it will return as if the param name was 'statuses'. + + Note: This seems to only work for Cancelled, Booked, CheckedIn, and Waitlisted statuses. If you provide + a different status, it will return all bookings, not filtered by status. The results in this scenario do + not line up with the `get_bookings` with no status provided, as that returns fewer records. Likely the + filtered dates are different on the backend. + + My guess: the endpoint called with dates and 'statuses' is a "v2" kind of thing, where they upgraded without + changing the version of the api. Calling it with no dates and a singular (limited) status is probably v1. + + I'm leaving this in here for reference, but marking it private. I just don't want to have to puzzle over + this again if I remove it and forget about it. + """ if status and status not in [ @@ -173,8 +179,6 @@ async def get_challenge_tracker_detail( ) -> ChallengeTrackerDetailList: """Get the member's challenge tracker details. - Note: I'm not sure what the challenge_sub_type_id is supposed to be, so it defaults to 0. - Args: equipment_id (EquipmentType): The equipment ID. challenge_type_id (ChallengeType): The challenge type ID. @@ -182,6 +186,11 @@ async def get_challenge_tracker_detail( Returns: ChallengeTrackerDetailList: The member's challenge tracker details. + + Notes: + --- + I'm not sure what the challenge_sub_type_id is supposed to be, so it defaults to 0. + """ params = { "equipmentId": equipment_id.value, @@ -198,14 +207,17 @@ async def get_challenge_tracker_detail( async def get_challenge_tracker_participation(self, challenge_type_id: ChallengeType) -> typing.Any: """Get the member's participation in a challenge. - Note: I've never gotten this to return anything other than invalid response. I'm not sure if it's a bug - in my code or the API. - Args: challenge_type_id (ChallengeType): The challenge type ID. Returns: - dict: The member's participation in the challenge. + Any: The member's participation in the challenge. + + Notes: + --- + I've never gotten this to return anything other than invalid response. I'm not sure if it's a bug + in my code or the API. + """ params = {"challengeTypeId": challenge_type_id.value} @@ -219,15 +231,6 @@ async def get_member_detail( ) -> MemberDetail: """Get the member details. - The member_id parameter is optional. If not provided, the currently logged in user will be used. The - include_addresses, include_class_summary, and include_credit_card parameters are optional and determine - what additional information is included in the response. By default, all additional information is included, - with the exception of the credit card information. - - Note: The base member details include the last four of a credit card regardless of the include_credit_card, - although this is not always the same details as what is in the member_credit_card field. There doesn't seem - to be a way to exclude this information, and I do not know which is which or why they differ. - Args: include_addresses (bool): Whether to include the member's addresses in the response. include_class_summary (bool): Whether to include the member's class summary in the response. @@ -235,6 +238,17 @@ async def get_member_detail( Returns: MemberDetail: The member details. + + + Notes: + --- + The include_addresses, include_class_summary, and include_credit_card parameters are optional and determine + what additional information is included in the response. By default, all additional information is included, + with the exception of the credit card information. + + The base member details include the last four of a credit card regardless of the include_credit_card, + although this is not always the same details as what is in the member_credit_card field. There doesn't seem + to be a way to exclude this information, and I do not know which is which or why they differ. """ include: list[str] = [] @@ -294,10 +308,13 @@ async def get_favorite_studios(self) -> FavoriteStudioList: async def get_latest_agreement(self) -> LatestAgreement: """Get the latest agreement for the member. - Note: latest agreement here means a specific agreement id, not the most recent agreement. - Returns: LatestAgreement: The agreement. + + Notes: + --- + In this context, "latest" means the most recent agreement with a specific ID, not the most recent agreement + in general. The agreement ID is hardcoded in the endpoint, so it will always return the same agreement. """ data = await self._api._default_request("GET", "/member/agreements/9d98fb27-0f00-4598-ad08-5b1655a59af6") return LatestAgreement(**data["data"]) @@ -319,16 +336,14 @@ async def get_studio_services(self, studio_uuid: str | None = None) -> StudioSer # the below do not return any data for me, so I can't test them - async def get_member_services(self, active_only: bool = True) -> typing.Any: + async def _get_member_services(self, active_only: bool = True) -> typing.Any: """Get the member's services. - Note: I'm not sure what the services are, as I don't have any data to test this with. - Args: active_only (bool): Whether to only include active services. Default is True. Returns: - dict: The member's service + Any: The member's service .""" active_only_str = "true" if active_only else "false" data = await self._api._default_request( @@ -336,7 +351,7 @@ async def get_member_services(self, active_only: bool = True) -> typing.Any: ) return data - async def get_aspire_data(self, datetime: str | None = None, unit: str | None = None) -> typing.Any: + async def _get_aspire_data(self, datetime: str | None = None, unit: str | None = None) -> typing.Any: """Get data from the member's aspire wearable. Note: I don't have an aspire wearable, so I can't test this. @@ -346,20 +361,20 @@ async def get_aspire_data(self, datetime: str | None = None, unit: str | None = unit (str | None): The measurement unit. Default is None. Returns: - dict: The member's aspire data. + Any: The member's aspire data. """ params = {"datetime": datetime, "unit": unit} data = self._api._default_request("GET", f"/member/wearables/{self._member_id}/wearable-daily", params=params) return data - async def get_body_composition_list(self) -> typing.Any: + async def _get_body_composition_list(self) -> typing.Any: """Get the member's body composition list. Note: I don't have body composition data, so I can't test this. Returns: - dict: The member's body composition list. + Any: The member's body composition list. """ data = await self._api._default_request("GET", f"/member/members/{self._member_uuid}/body-composition") return data diff --git a/src/otf_api/models/__init__.py b/src/otf_api/models/__init__.py index 9770ebd..4bc040f 100644 --- a/src/otf_api/models/__init__.py +++ b/src/otf_api/models/__init__.py @@ -8,9 +8,6 @@ ChallengeTrackerContent, ChallengeTrackerDetailList, ChallengeType, - DnaHrHistory, - DnaMaxHr, - DnaTelemetry, EquipmentType, FavoriteStudioList, HistoryClassStatus, @@ -26,6 +23,9 @@ StudioDetailList, StudioServiceList, StudioStatus, + TelemetryHrHistory, + TelemetryItem, + TelemetryMaxHr, TotalClasses, WorkoutList, ) @@ -50,9 +50,9 @@ "WorkoutList", "FavoriteStudioList", "OtfClassList", - "DnaHrHistory", - "DnaTelemetry", - "DnaMaxHr", + "TelemetryHrHistory", + "TelemetryItem", + "TelemetryMaxHr", "StudioDetail", "StudioDetailList", "ALL_CLASS_STATUS", diff --git a/src/otf_api/models/responses/__init__.py b/src/otf_api/models/responses/__init__.py index a0a7734..083afaf 100644 --- a/src/otf_api/models/responses/__init__.py +++ b/src/otf_api/models/responses/__init__.py @@ -2,9 +2,6 @@ from .challenge_tracker_content import ChallengeTrackerContent from .challenge_tracker_detail import ChallengeTrackerDetailList from .classes import OtfClassList -from .dna_hr_history import DnaHrHistory -from .dna_max_hr import DnaMaxHr -from .dna_telemetry import DnaTelemetry from .enums import ( ALL_CLASS_STATUS, ALL_HISTORY_CLASS_STATUS, @@ -25,6 +22,9 @@ from .performance_summary_list import PerformanceSummaryList from .studio_detail import StudioDetail, StudioDetailList from .studio_services import StudioServiceList +from .telemetry_hr_history import TelemetryHrHistory +from .telemetry_item import TelemetryItem +from .telemetry_max_hr import TelemetryMaxHr from .total_classes import TotalClasses from .workouts import WorkoutList @@ -47,9 +47,9 @@ "StudioStatus", "FavoriteStudioList", "OtfClassList", - "DnaHrHistory", - "DnaTelemetry", - "DnaMaxHr", + "TelemetryHrHistory", + "TelemetryItem", + "TelemetryMaxHr", "StudioDetail", "StudioDetailList", "ALL_CLASS_STATUS", diff --git a/src/otf_api/models/responses/dna_hr_history.py b/src/otf_api/models/responses/telemetry_hr_history.py similarity index 94% rename from src/otf_api/models/responses/dna_hr_history.py rename to src/otf_api/models/responses/telemetry_hr_history.py index df3424d..9a6b4ce 100644 --- a/src/otf_api/models/responses/dna_hr_history.py +++ b/src/otf_api/models/responses/telemetry_hr_history.py @@ -29,6 +29,6 @@ class HistoryItem(OtfBaseModel): assigned_at: str = Field(..., alias="assignedAt") -class DnaHrHistory(OtfBaseModel): +class TelemetryHrHistory(OtfBaseModel): member_uuid: str = Field(..., alias="memberUuid") history: list[HistoryItem] diff --git a/src/otf_api/models/responses/dna_telemetry.py b/src/otf_api/models/responses/telemetry_item.py similarity index 97% rename from src/otf_api/models/responses/dna_telemetry.py rename to src/otf_api/models/responses/telemetry_item.py index a812604..d071e4b 100644 --- a/src/otf_api/models/responses/dna_telemetry.py +++ b/src/otf_api/models/responses/telemetry_item.py @@ -38,7 +38,7 @@ class TelemetryItem(OtfBaseModel): tread_data: TreadData | None = Field(None, alias="treadData") -class DnaTelemetry(OtfBaseModel): +class TelemetryItem(OtfBaseModel): member_uuid: str = Field(..., alias="memberUuid") class_history_uuid: str = Field(..., alias="classHistoryUuid") class_start_time: datetime = Field(..., alias="classStartTime") diff --git a/src/otf_api/models/responses/dna_max_hr.py b/src/otf_api/models/responses/telemetry_max_hr.py similarity index 86% rename from src/otf_api/models/responses/dna_max_hr.py rename to src/otf_api/models/responses/telemetry_max_hr.py index 7c75353..a4acd6a 100644 --- a/src/otf_api/models/responses/dna_max_hr.py +++ b/src/otf_api/models/responses/telemetry_max_hr.py @@ -8,6 +8,6 @@ class MaxHr(OtfBaseModel): value: int -class DnaMaxHr(OtfBaseModel): +class TelemetryMaxHr(OtfBaseModel): member_uuid: str = Field(..., alias="memberUuid") max_hr: MaxHr = Field(..., alias="maxHr") diff --git a/src/otf_api/performance_api.py b/src/otf_api/performance_api.py index 55b480a..fa8f6ab 100644 --- a/src/otf_api/performance_api.py +++ b/src/otf_api/performance_api.py @@ -18,7 +18,20 @@ def __init__(self, api: "Api"): self._headers = {"koji-member-id": self._member_id, "koji-member-email": self._api.user.id_claims_data.email} async def get_performance_summaries(self, limit: int = 30) -> PerformanceSummaryList: - # note: in the app this is referred to as 'getInStudioWorkoutHistory' + """Get a list of performance summaries for the authenticated user. + + Args: + limit (int): The maximum number of performance summaries to return. Defaults to 30. + + Returns: + PerformanceSummaryList: A list of performance summaries. + + Developer Notes: + --- + In the app, this is referred to as 'getInStudioWorkoutHistory'. + + """ + path = "/v1/performance-summaries" params = {"limit": limit} res = await self._api._performance_summary_request("GET", path, headers=self._headers, params=params) @@ -26,6 +39,15 @@ async def get_performance_summaries(self, limit: int = 30) -> PerformanceSummary return retval async def get_performance_summary(self, performance_summary_id: str) -> PerformanceSummaryDetail: + """Get a detailed performance summary for a given workout. + + Args: + performance_summary_id (str): The ID of the performance summary to retrieve. + + Returns: + PerformanceSummaryDetail: A detailed performance summary. + """ + path = f"/v1/performance-summaries/{performance_summary_id}" res = await self._api._performance_summary_request("GET", path, headers=self._headers) retval = PerformanceSummaryDetail(**res) diff --git a/src/otf_api/studio_api.py b/src/otf_api/studios_api.py similarity index 90% rename from src/otf_api/studio_api.py rename to src/otf_api/studios_api.py index 6d8b197..b112d62 100644 --- a/src/otf_api/studio_api.py +++ b/src/otf_api/studios_api.py @@ -42,10 +42,7 @@ async def search_studios_by_geo( page_index: int = 1, page_size: int = 50, ) -> StudioDetailList: - """Search for studios by geographic location. Requires latitude and longitude, other parameters are optional. - - There does not seem to be a limit to the number of results that can be requested total or per page, the library - enforces a limit of 50 results per page to avoid potential rate limiting issues. + """Search for studios by geographic location. Args: latitude (float, optional): Latitude of the location to search around, if None uses home studio latitude. @@ -57,6 +54,10 @@ async def search_studios_by_geo( Returns: StudioDetailList: List of studios that match the search criteria. + Notes: + --- + There does not seem to be a limit to the number of results that can be requested total or per page, the + library enforces a limit of 50 results per page to avoid potential rate limiting issues. """ path = "/mobile/v1/studios" diff --git a/src/otf_api/dna_api.py b/src/otf_api/telemetry_api.py similarity index 75% rename from src/otf_api/dna_api.py rename to src/otf_api/telemetry_api.py index c358848..86d1b78 100644 --- a/src/otf_api/dna_api.py +++ b/src/otf_api/telemetry_api.py @@ -1,15 +1,15 @@ import typing from math import ceil -from otf_api.models.responses.dna_hr_history import DnaHrHistory -from otf_api.models.responses.dna_max_hr import DnaMaxHr -from otf_api.models.responses.dna_telemetry import DnaTelemetry +from otf_api.models.responses.telemetry_hr_history import TelemetryHrHistory +from otf_api.models.responses.telemetry_item import TelemetryItem +from otf_api.models.responses.telemetry_max_hr import TelemetryMaxHr if typing.TYPE_CHECKING: from otf_api import Api -class DnaApi: +class TelemtryApi: def __init__(self, api: "Api"): self._api = api self.logger = api.logger @@ -18,38 +18,38 @@ def __init__(self, api: "Api"): self._member_id = self._api.user.member_id self._member_uuid = self._api.user.member_uuid - async def get_hr_history(self) -> DnaHrHistory: + async def get_hr_history(self) -> TelemetryHrHistory: """Get the heartrate history for the user. Returns a list of history items that contain the max heartrate, start/end bpm for each zone, the change from the previous, the change bucket, and the assigned at time. Returns: - DnaHrHistory: The heartrate history for the user. + TelemetryHrHistory: The heartrate history for the user. """ path = "/v1/physVars/maxHr/history" params = {"memberUuid": self._member_id} - res = await self._api._dna_request("GET", path, params=params) - return DnaHrHistory(**res) + res = await self._api._telemetry_request("GET", path, params=params) + return TelemetryHrHistory(**res) - async def get_max_hr(self) -> DnaMaxHr: + async def get_max_hr(self) -> TelemetryMaxHr: """Get the max heartrate for the user. Returns a simple object that has the member_uuid and the max_hr. Returns: - DnaMaxHr: The max heartrate for the user. + TelemetryMaxHr: The max heartrate for the user. """ path = "/v1/physVars/maxHr" params = {"memberUuid": self._member_id} - res = await self._api._dna_request("GET", path, params=params) - return DnaMaxHr(**res) + res = await self._api._telemetry_request("GET", path, params=params) + return TelemetryMaxHr(**res) - async def get_telemetry(self, class_history_uuid: str, max_data_points: int = 0) -> DnaTelemetry: + async def get_telemetry(self, class_history_uuid: str, max_data_points: int = 0) -> TelemetryItem: """Get the telemetry for a class history. This returns an object that contains the max heartrate, start/end bpm for each zone, @@ -61,7 +61,7 @@ async def get_telemetry(self, class_history_uuid: str, max_data_points: int = 0) get the max data points from the workout. If the workout is not found, it will default to 120 data points. Returns: - DnaTelemetry: The telemetry for the class history. + TelemetryItem: The telemetry for the class history. """ path = "/v1/performance/summary" @@ -69,8 +69,8 @@ async def get_telemetry(self, class_history_uuid: str, max_data_points: int = 0) max_data_points = max_data_points or await self._get_max_data_points(class_history_uuid) params = {"classHistoryUuid": class_history_uuid, "maxDataPoints": max_data_points} - res = await self._api._dna_request("GET", path, params=params) - return DnaTelemetry(**res) + res = await self._api._telemetry_request("GET", path, params=params) + return TelemetryItem(**res) async def _get_max_data_points(self, class_history_uuid: str) -> int: """Get the max data points to use for the telemetry.