diff --git a/.github/workflows/push-test.yml b/.github/workflows/push-test.yml index a183dd9..5fb5f23 100644 --- a/.github/workflows/push-test.yml +++ b/.github/workflows/push-test.yml @@ -5,7 +5,7 @@ name: Test Package and TestPyPI on: push: branches: - - dev_env + - '*' jobs: test-package: diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml new file mode 100644 index 0000000..904571e --- /dev/null +++ b/.github/workflows/update-readme.yml @@ -0,0 +1,32 @@ +name: Update README Badges + +on: + push: + branches: + - '*' + pull_request: + branches: + - '*' + +jobs: + update-readme: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Update README badges + run: | + sed -i "s|branch=.*)](https://github.com/HawkinDynamics/hawkinPy/actions/workflows/push-test.yml)|branch=${GITHUB_REF#refs/heads/})](https://github.com/HawkinDynamics/hawkinPy/actions/workflows/push-test.yml)|" README.md + sed -i "s|hawkinPy/.*)](https://img.shields.io/github/last-commit/HawkinDynamics/hawkinPy/)|hawkinPy/${GITHUB_REF#refs/heads/})](https://img.shields.io/github/last-commit/HawkinDynamics/hawkinPy/)|" README.md + + - name: Commit and push changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add README.md + git commit -m "Update README badges with branch ${GITHUB_REF#refs/heads/}" + git push + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 35a22f0..ab63767 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ MANIFEST # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec +get-pip.py # Installer logs pip-log.txt @@ -161,4 +162,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +#.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 93e56d0..b68faec 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# HDFORCE +# HDFORCE v1.1.2 **Get your data from the Hawkin Dynamics API** ![GitHub Release](https://img.shields.io/github/v/release/HawkinDynamics/hawkinPy) -[![Test Py Versions and OS](https://github.com/HawkinDynamics/hawkinPy/actions/workflows/push-test.yml/badge.svg?branch=dev_env)](https://github.com/HawkinDynamics/hawkinPy/actions/workflows/push-test.yml) -![GitHub last commit (branch)](https://img.shields.io/github/last-commit/HawkinDynamics/hawkinPy/dev_env) +[![Test Py Versions and OS](https://github.com/HawkinDynamics/hawkinPy/actions/workflows/push-test.yml/badge.svg?branch=main)](https://github.com/HawkinDynamics/hawkinPy/actions/workflows/push-test.yml) +![GitHub last commit (branch)](https://img.shields.io/github/last-commit/HawkinDynamics/hawkinPy/main) [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) [![lifecycle](https://img.shields.io/badge/lifecycle-stable-green.svg)](https://www.tidyverse.org/lifecycle/#stable) [![license](https://img.shields.io/badge/license-MIT%20+%20file%20LICENSE-lightgrey.svg)](https://choosealicense.com/) @@ -16,7 +16,7 @@ HDFORCE provides simple functionality with Hawkin Dynamics API. These functions are for use with ‘Hawkin Dynamics Beta API’ version 1.10-beta. You must be a Hawkin Dynamics user with an active integration account to utilize functions within the package. ## Functions -This API is designed to get data out of your Hawkin Dynamics server and interact with your data more intimately. It is not designed to be accessed from client applications directly. There is a limit on the amount of data that can be returned in a single request (256 MB). As your database grows, it will be necessary to use the `from` and `to` parameters to limit the size of the responses. Responses that exceed the memory limit will timeout and fail. It is advised that you design your client to handle this from the beginning. A recommended pattern would be to have two methods of fetching data. A scheduled pull that uses the `from` and `to` parameters to constrain the returned data to only tests that have occurred since the last fetch e.g. every day or every 5 minutes. And then a pull that fetches the entire database since you began testing that is only executed when necessary. A recommended way of doing this is to generate the `from` and `to` parameters for each month since you started and send a request for each either in parallel or sequentially. +This API is designed to get data out of your Hawkin Dynamics server and interact with your data more intimately. It is not designed to be accessed from client applications directly. There is a limit on the amount of data that can be returned in a single request (256 MB). As your database grows, it will be necessary to use the `from_` and `to_` parameters to limit the size of the responses. Responses that exceed the memory limit will timeout and fail. It is advised that you design your client to handle this from the beginning. A recommended pattern would be to have two methods of fetching data. A scheduled pull that uses the `from_` and `to_` parameters to constrain the returned data to only tests that have occurred since the last fetch e.g. every day or every 5 minutes. And then a pull that fetches the entire database since you began testing that is only executed when necessary. A recommended way of doing this is to generate the `from_` and `to_` parameters for each month since you started and send a request for each either in parallel or sequentially. This package was meant to help execute requests to the Hawkin Dynamics API with a single line of code. There are 13 functions to help execute 4 primary objectives: diff --git a/docs/About/changelog.md b/docs/About/changelog.md index 48173c0..503901e 100644 --- a/docs/About/changelog.md +++ b/docs/About/changelog.md @@ -1,5 +1,25 @@ # Changelogs +## hdforce v1.1.2 + +* Bug fix: addition of new TruStrength test names and IDs to testTypeId validation method + +## hdforce v1.1.1 + +* Corrected versioning and documentation + +## hdforce v1.1.0 + +* Additions of CreateAthlete and UpdateAthlete functions +* Expansion of GetTests function to include 'team', 'group', type', and 'athlete' arguments +* Deprecation of GetTestsAth, GetTestsType, GetTestsTeam, and GetTestsGroup + +## hdforce v1.0.01 + +* Initial release of production package +* Full logging configuration and Authentication features +* Tested on Python version 3.9 <-> 3.12, on Mac, Windows, and Linux + ## hdforce v1.0.0rc0 * Improved logging diff --git a/docs/Functions/CreateAthletes.md b/docs/Functions/CreateAthletes.md new file mode 100644 index 0000000..78db115 --- /dev/null +++ b/docs/Functions/CreateAthletes.md @@ -0,0 +1,56 @@ +__`CreateAthletes(athletes: List[NewAthletes])`__ + +### Description +Create athletes for your account. Up to 500 at one time. + +### Parameters +`athletes`: (_list_) A list of Athletes with class of `NewAthlete`, which requires a "name". If other parameters are left, they will assume default values. + +### Classes +`NewAthlete`: (_class_) +**REQUIRED** +* name: _str_ +* active: _str_ +*Optional* +* teams: _list_ +* groups: _list_ +* external: _dict_ {externalName1 : externalValue1, externalName2 : externalValue2} + +### Returns +A list of AthleteResult objects indicating the success or failure of each athlete creation. + +* __successful__: list of names of athletes added successfully +* __failures__: list of athletes that failed in execution, grouped by their reason for failure. + +### Raises +**Exception** + +* No Access Token Found. +* If the HTTP response status is not 200, indicating an unsuccessful API request, or if there is a failure in parsing the JSON response. + + +### Example + +``` Python +from hdforce import CreateAthletes, NewAthlete + +# Create list of athletes to add using NewAthlete class +players = [ + NewAthlete(name= "New Guy", active=False, teams=[], groups=[], external={"Title": "Younger Brother"}), + NewAthlete(name= "Old Guy", active=False, teams=[], groups=[], external={"Title": "Older Brother"}) +] + +# Create players +newRoster = CreateAthletes(athletes = players) + +# Print lgCreation Response +print(newRoster) +``` + +_output_ + +{ + "successful": ["New Guy", "Old Guy"], + "failures": [] +} + diff --git a/docs/Functions/GetAthletes.md b/docs/Functions/GetAthletes.md index 24aa032..1fa1eb4 100644 --- a/docs/Functions/GetAthletes.md +++ b/docs/Functions/GetAthletes.md @@ -43,4 +43,3 @@ _output_ |----------------------|--------------|--------------------------------------------------|----------------------|--------|-------------------|-------------------|--------------------|--------------------| | OLbsebtmf81eiwg1AeE5 | Lauren Green | ['DPMb6ek2mgUNVcg8siSqpnIvE2i2', 'vW9iEKafhs2PamfWQpFZ'] | ['yh8RnOvg56dQNrZGBKWZ'] | True | 2004 | Whittier | 83keo9wjei939ekd9 | SA0042643 | - diff --git a/docs/Functions/GetForceTime.md b/docs/Functions/GetForceTime.md index cbc596a..b932e34 100644 --- a/docs/Functions/GetForceTime.md +++ b/docs/Functions/GetForceTime.md @@ -48,4 +48,4 @@ _output_ | 2001 | 2.002 | 46 | 122 | 168 | -1.128090 | -0.078543 | -189.519185 | [] | | 2002 | 2.003 | 49 | 126 | 175 | -1.136487 | -0.079675 | -198.885184 | [] | | 2003 | 2.004 | 53 | 130 | 183 | -1.144821 | -0.080816 | -209.502302 | [] | -| 2004 | 2.005 | 56 | 135 | 191 | -1.153090 | -0.081965 | -220.240178 | [] | +| 2004 | 2.005 | 56 | 135 | 191 | -1.153090 | -0.081965 | -220.240178 | [] | \ No newline at end of file diff --git a/docs/Functions/GetMetrics.md b/docs/Functions/GetMetrics.md index 027f8b3..039e875 100644 --- a/docs/Functions/GetMetrics.md +++ b/docs/Functions/GetMetrics.md @@ -40,4 +40,4 @@ _output_ | 7nNduHeM5zETPjHxvm7s | Countermovement Jump | peakRelativeBrakingForce | Peak Relative Braking Force | % | The peak instantaneous vertical ground reaction force applied to the system center of mass during the braking phase as a percentage of system weight. | | 7nNduHeM5zETPjHxvm7s | Countermovement Jump | avgPropulsiveForce | Avg. Propulsive Force | N | The average vertical ground reaction force applied to the system center of mass during the propulsion phase. | | 7nNduHeM5zETPjHxvm7s | Countermovement Jump | avgRelativePropulsiveForce| Avg. Relative Propulsive Force | % | The average vertical ground reaction force applied to the system center of mass during the propulsion phase as a percentage of system weight. | -| 7nNduHeM5zETPjHxvm7s | Countermovement Jump | peakPropulsiveForce | Peak Propulsive Force | N | The peak instantaneous vertical ground reaction force applied to the system center of mass during the propulsion phase. | +| 7nNduHeM5zETPjHxvm7s | Countermovement Jump | peakPropulsiveForce | Peak Propulsive Force | N | The peak instantaneous vertical ground reaction force applied to the system center of mass during the propulsion phase. | \ No newline at end of file diff --git a/docs/Functions/GetTags.md b/docs/Functions/GetTags.md index 02299f9..4924b37 100644 --- a/docs/Functions/GetTags.md +++ b/docs/Functions/GetTags.md @@ -35,4 +35,4 @@ _output_ | Afcw45lIkeHFnlUyDeSn | 10/5 | 10/5 Multi-Rebound test protocol | | KmZmhxUqrbOhLLvRLAbG | 5/3 | 5/3 Multi-Rebound test protocol | | Lgc8uJh80NacB8eaqjOg | 20kg | addition of 20kg load to system mass | -| hVgWJkwHZ9Mm8XDymP3W | Hands On Hips | Akimbo style jump | +| hVgWJkwHZ9Mm8XDymP3W | Hands On Hips | Akimbo style jump | \ No newline at end of file diff --git a/docs/Functions/GetTests.md b/docs/Functions/GetTests.md index 7b91998..cb8192b 100644 --- a/docs/Functions/GetTests.md +++ b/docs/Functions/GetTests.md @@ -1,4 +1,4 @@ -__`GetTests(from_: int = None, to_: int = None, sync: bool = False, active: bool = True)`__ +__`GetTests(from_: int = None, to_: int = None, sync: bool = False, athleteId: str = None, typeId: str = None, teamId: str = None,groupId: str = None, includeInactive: bool = False)`__ ### Description Get all test trials from an account. Allows filtering of results based on time frames, synchronization needs, and the active status of tests. @@ -10,7 +10,15 @@ __`to_`__: _(int)_ Unix timestamp specifying the end time until which tests shou __`sync`__: _(bool)_ If True, the function fetches updated and newly created tests to synchronize with the database. Default is False. -__`active`__: _(bool)_ If True, only active tests are fetched. If False, all tests including inactive ones are fetched. Default is True. +__`athleteId`__: _(str)_ The unique identifier of the athlete whose tests are to be retrieved. + +__`typeId`__: _(str)_ The canonical test ID, test type name, or test name abbreviation. Must correspond to known test types. + +__`teamId`__: _(str)_ A single team ID, tuple or list of team IDs to receive tests from specific teams. + +__`groupId`__: _(str)_ A single group ID, tuple or list of group IDs to receive tests from specific groups. + +__`includeInactive`__: _(bool)_ Default to False, where only active tests are returned. If True, all tests including inactive ones are returned. ### Returns A Pandas DataFrame containing details of the test trial, with columns: diff --git a/docs/Functions/GetTestsAth.md b/docs/Functions/GetTestsAth.md index bef3ede..fef13e1 100644 --- a/docs/Functions/GetTestsAth.md +++ b/docs/Functions/GetTestsAth.md @@ -1,4 +1,7 @@ -__`GetTestsAth(athleteId: str, from_: int = None, to_: int = None, sync: bool = False, active: bool = True)`__ +__`GetTestsAth(athleteId: str, from_: int = None, to_: int = None, sync: bool = False, includeInactive: bool = False)`__ + +> As of July 10, 2024 `GetTestsTeam` has been deprecated for the preferred use +> of `GetTests`. This function will be fully superseded Jan 01, 2025 12:00:00. ### Description Get test trials only from a specific athlete. Allows filtering of results based on time frames, synchronization needs, and the active status of tests. @@ -12,7 +15,7 @@ __`to_`__: _(int)_ Unix timestamp specifying the end time until which tests shou __`sync`__: _(bool)_ If True, the function fetches updated and newly created tests to synchronize with the database. Default is False. -__`active`__: _(bool)_ If True, only active tests are fetched. If False, all tests including inactive ones are fetched. Default is True. +__`includeInactive`__: _(bool)_ Default to False, where only active tests are returned. If True, all tests including inactive ones are returned. ### Returns A Pandas DataFrame containing details of the test trial, with columns: diff --git a/docs/Functions/GetTestsGroup.md b/docs/Functions/GetTestsGroup.md index 4965aab..1716bb6 100644 --- a/docs/Functions/GetTestsGroup.md +++ b/docs/Functions/GetTestsGroup.md @@ -1,4 +1,7 @@ -__`GetTestsGroup(groupId: str, from_: int = None, to_: int = None, sync: bool = False, active: bool = True)`__ +__`GetTestsGroup(groupId: str, from_: int = None, to_: int = None, sync: bool = False, includeInactive: bool = False)`__ + +> As of July 10, 2024 `GetTestsTeam` has been deprecated for the preferred use +> of `GetTests`. This function will be fully superseded Jan 01, 2025 12:00:00. ### Description Get test trials for specified group(s). Allows filtering of results based on time frames, synchronization needs, and the active status of tests. @@ -12,7 +15,7 @@ __`to_`__: _(int)_ Unix timestamp specifying the end time until which tests shou __`sync`__: _(bool)_ If True, the function fetches updated and newly created tests to synchronize with the database. Default is False. -__`active`__: _(bool)_ If True, only active tests are fetched. If False, all tests including inactive ones are fetched. Default is True. +__`includeInactive`__: _(bool)_ Default to False, where only active tests are returned. If True, all tests including inactive ones are returned. ### Returns A Pandas DataFrame containing details of the test trial, with columns: diff --git a/docs/Functions/GetTestsTeam.md b/docs/Functions/GetTestsTeam.md index b9fbb63..e13430e 100644 --- a/docs/Functions/GetTestsTeam.md +++ b/docs/Functions/GetTestsTeam.md @@ -1,4 +1,7 @@ -__`GetTestsTeam(teamId: str, from_: int = None, to_: int = None, sync: bool = False, active: bool = True)`__ +__`GetTestsTeam(teamId: str, from_: int = None, to_: int = None, sync: bool = False, includeInactive: bool = False)`__ + +> As of July 10, 2024 `GetTestsTeam` has been deprecated for the preferred use +> of `GetTests`. This function will be fully superseded Jan 01, 2025 12:00:00. ### Description Get test trials for specified team(s). Allows filtering of results based on time frames, synchronization needs, and the active status of tests. @@ -12,7 +15,7 @@ __`to_`__: _(int)_ Unix timestamp specifying the end time until which tests shou __`sync`__: _(bool)_ If True, the function fetches updated and newly created tests to synchronize with the database. Default is False. -__`active`__: _(bool)_ If True, only active tests are fetched. If False, all tests including inactive ones are fetched. Default is True. +__`includeInactive`__: _(bool)_ Default to False, where only active tests are returned. If True, all tests including inactive ones are returned. ### Returns A Pandas DataFrame containing details of the test trial, with columns: diff --git a/docs/Functions/GetTestsType.md b/docs/Functions/GetTestsType.md index d6cfb1a..60a3e04 100644 --- a/docs/Functions/GetTestsType.md +++ b/docs/Functions/GetTestsType.md @@ -1,4 +1,7 @@ -__`GetTestsType(typeId: str, from_: int = None, to_: int = None, sync: bool = False, active: bool = True)`__ +__`GetTestsType(typeId: str, from_: int = None, to_: int = None, sync: bool = False, includeInactive: bool = False)`__ + +> As of July 10, 2024 `GetTestsTeam` has been deprecated for the preferred use +> of `GetTests`. This function will be fully superseded Jan 01, 2025 12:00:00. ### Description Get test trials only from a specific type of test. Allows filtering of results based on time frames, synchronization needs, and the active status of tests. @@ -16,6 +19,8 @@ The typeId has been created to be more user friendly, as it accepts any of canon | r4fhrkPdYlLxYQxEeM78 | Multi Rebound | MR | | ubeWMPN1lJFbuQbAM97s | Weigh In | WI | | rKgI4y3ItTAzUekTUpvR | Drop Landing | DL | +| 4KlQgKmBxbOY6uKTLDFL | TS Free Run | TSFR | +| umnEZPgi6zaxuw0KhUpM | TS Isometric Test | TSISO | ### Parameters __`typeId`__: _(str)_ The canonical test ID, test type name, or test name abbreviation. Must correspond to known test types. @@ -26,7 +31,7 @@ __`to_`__: _(int)_ Unix timestamp specifying the end time until which tests shou __`sync`__: _(bool)_ If True, the function fetches updated and newly created tests to synchronize with the database. Default is False. -__`active`__: _(bool)_ If True, only active tests are fetched. If False, all tests including inactive ones are fetched. Default is True. +__`includeInactive`__: _(bool)_ If True, only active tests are fetched. If False, all tests including inactive ones are fetched. Default is True. ### Returns A Pandas DataFrame containing details of the test trial, with columns: diff --git a/docs/Functions/GetTypes.md b/docs/Functions/GetTypes.md index c886b73..3450c97 100644 --- a/docs/Functions/GetTypes.md +++ b/docs/Functions/GetTypes.md @@ -29,17 +29,16 @@ print(testTypes) _output_ -| id | name | -|----------------------|--------------------| +| id | name | +|----------------------|----------------------| | 7nNduHeM5zETPjHxvm7s | Countermovement Jump | -| QEG7m7DhYsD6BrcQ8pic | Squat Jump | -| 2uS5XD5kXmWgIZ5HhQ3A | Isometric Test | -| gyBETpRXpdr63Ab2E0V8 | Drop Jump | -| 5pRSUQVSJVnxijpPMck3 | Free Run | -| pqgf2TPUOQOQs6r0HQWb | CMJ Rebound | -| r4fhrkPdYlLxYQxEeM78 | Multi Rebound | -| ubeWMPN1lJFbuQbAM97s | Weigh In | -| rKgI4y3ItTAzUekTUpvR | Drop Landing | - - - +| QEG7m7DhYsD6BrcQ8pic | Squat Jump | +| 2uS5XD5kXmWgIZ5HhQ3A | Isometric Test | +| gyBETpRXpdr63Ab2E0V8 | Drop Jump | +| 5pRSUQVSJVnxijpPMck3 | Free Run | +| pqgf2TPUOQOQs6r0HQWb | CMJ Rebound | +| r4fhrkPdYlLxYQxEeM78 | Multi Rebound | +| ubeWMPN1lJFbuQbAM97s | Weigh In | +| rKgI4y3ItTAzUekTUpvR | Drop Landing | +| 4KlQgKmBxbOY6uKTLDFL | TS Free Run | +| umnEZPgi6zaxuw0KhUpM | TS Isometric Test | diff --git a/docs/Functions/UpdateAthletes.md b/docs/Functions/UpdateAthletes.md new file mode 100644 index 0000000..e92ed71 --- /dev/null +++ b/docs/Functions/UpdateAthletes.md @@ -0,0 +1,54 @@ +__`UpdateAthletes(athletes: List[Athletes])`__ + +### Description +Update athletes for your account. Up to 500 at one time. + +### Parameters +`athletes`: (_list_) A list of Athletes with class of `Athlete`, which requires an "id", "name", and "active" for eah athlete entered. Any parameters omitted will retain their current values. Except for "external", which will be removed unless explicitly stated during update. + +### Classes +`Athlete`: (_class_) +**REQUIRED** +* id: _str_ +* name: _str_ +* active: _str_ +*Optional* +* teams: _list_ +* groups: _list_ +* external: _dict_ {externalName1 : externalValue1, externalName2 : externalValue2} + +### Returns +A list of AthleteResult objects indicating the success or failure of each athlete creation. + +* __AthleteResult__: Class with athlete name, id, success status, and reason. + +### Raises +**Exception** + +* No Access Token Found. +* If the HTTP response status is not 200, indicating an unsuccessful API request, or if there is a failure in parsing the JSON response. + +### Example + +``` Python +from hdforce import UpdateAthletes, Athlete + +# Create list of athletes to add using NewAthlete class +players = [ + Athlete(id= "N3wGuy$Un1q131D", name= "New Guy", active=False, external={"Title": "Younger Brother"}), + Athlete(id= "0ldGuy$Un1q131D", name= "Old Guy", active=False, external={"Title": "Older Brother"}) +] + +# Create players +updates = UpdateAthletes(athletes = players) + +# Print lgCreation Response +print(updates) +``` + +_output_ + +[ + AthleteResult(name= 'New Guy', id='N3wGuy$Un1q131D', successful=True, reason=None), + AthleteResult(name= 'Old Guy', id='0ldGuy$Un1q131D', successful=True, reason=None) +] \ No newline at end of file diff --git a/docs/UserGuide/GetData.md b/docs/UserGuide/GetData.md index c8f2b33..408944d 100644 --- a/docs/UserGuide/GetData.md +++ b/docs/UserGuide/GetData.md @@ -10,7 +10,7 @@ While the purpose of the package is to help with accessing data specific to your Every organization has data specific to them. With that, these entities will have unique IDs. It is important to have these IDs available to make the most of your test calls. -* `GetAthletes()` - Get the athletes for an account. Inactive players will only be included if `inactive` parameter is set to TRUE. The response will be a data frame containing the athletes that match this query. +* `GetAthletes()` - Get the athletes for an account. Inactive players will only be included if `includeInactive` parameter is set to TRUE. The response will be a data frame containing the athletes that match this query. * `GetTeams()` - Get the team names and IDs for all the teams in the org. The response will be a data frame containing the teams that are in the organization. * `GetGroups()` - Get the group names and IDs for all the groups in the org. The response will be a data frame containing the groups that are in the organization. * `GetTags()` - Get the tag names, IDs, and descriptions for tags created by users in your org. The response will be a data frame. @@ -27,11 +27,15 @@ value you will receive every test. This parameter is best suited for bulk export you will receive every test from the beginning of time or the optionally supplied `from_` parameter. This parameter is best suited for bulk exports of historical data * `sync` = The result set will include updated and newly created tests, following the time constraints of `from_` and `to_`. This parameter is best suited to keep your database in sync with the Hawkin database. It cannot and should not be used to fetch your entire database. A recommended strategy would be to have a job that runs on a short interval e.g. every five minutes that sends the `lastSyncTime` that it received as the `from_` parameter with `sync=True`. -* `active` = If True, only active tests are fetched. If False, all tests including inactive ones are fetched. The default is set to True. +* `includeInactive` = Default to False, where only active tests are returned. If True, all tests including inactive ones are returned. #### Get Test Function -* `GetTests()` - Get the tests for your account. You can specify a time frame `from_`, or `to_`, which the tests should come (or be synced). The Response will be a data frame containing the trials within the time range (if specified). +> As of July 10, 2024, `GetTestsAth`, `GetTestsType`, `GetTestsTeam`, and `GetTestsGroup` +> have been deprecated for the preferred use of `GetTests`. This function will be fully +> superseded Jan 01, 2025 12:00:00. + +* `GetTests()` - The primary function to call tests is `get_tests`. This is a base request for tests that, as of 2024-07-10, accepts all arguments : 'from', 'to', 'sync', 'includeInactive', 'athleteId', 'testTypeId', 'teamId', and 'groupId'. Using this function, you have complete control of the tests being requested from the cloud. It is important to understand that requests can **NOT** include any combination of 'athleteId', 'testTypeId', 'teamId', or 'groupId'. This will result in and error. Any of these arguments **CAN** be used with 'from', 'to', 'active', and 'sync'. * `GetTestsAth()` - Get only tests of the specified athlete from your organization. You can specify a time frame `from_`, or `to_`, which the tests should come (or be synced). Response will be a data frame containing the trials from the athlete, within the time range (if specified). * `GetTestsType()` - Get only tests of the specified test type from your organization. You can specify a time frame `from_`, or `to_`, which the tests should come (or be synced). Response will be a data frame containing the trials from that test type, within the time range (if specified). * `GetTestsTeam()` - Get only tests of the specified teams from your organization. Requires a `teamId` argument, which expects a text string, list or tuple (max of 10 teams). You can specify a time frame `from_`, or `to_`, which the tests should come (or be synced). Response will be a data frame containing the trials from those teams, within the time range (if specified). diff --git a/docs/index.md b/docs/index.md index 73f2f28..5f50f44 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,8 +3,8 @@ ![GitHub Release](https://img.shields.io/github/v/release/HawkinDynamics/hawkinPy) -[![Test Py Versions and OS](https://github.com/HawkinDynamics/hawkinPy/actions/workflows/push-test.yml/badge.svg?branch=dev_env)](https://github.com/HawkinDynamics/hawkinPy/actions/workflows/push-test.yml) -![GitHub last commit (branch)](https://img.shields.io/github/last-commit/HawkinDynamics/hawkinPy/dev_env) +[![Test Py Versions and OS](https://github.com/HawkinDynamics/hawkinPy/actions/workflows/push-test.yml/badge.svg?branch=main)](https://github.com/HawkinDynamics/hawkinPy/actions/workflows/push-test.yml) +![GitHub last commit (branch)](https://img.shields.io/github/last-commit/HawkinDynamics/hawkinPy/main) [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) [![lifecycle](https://img.shields.io/badge/lifecycle-stable-green.svg)](https://www.tidyverse.org/lifecycle/#stable) [![license](https://img.shields.io/badge/license-MIT%20+%20file%20LICENSE-lightgrey.svg)](https://choosealicense.com/) diff --git a/hdforce/AuthManager.py b/hdforce/AuthManager.py index ecdf953..d461d8d 100644 --- a/hdforce/AuthManager.py +++ b/hdforce/AuthManager.py @@ -167,4 +167,4 @@ def AuthManager(authMethod: str = "env", refreshToken_name: str = "HD_REFRESH_TO # Join the names of the empty variables into a comma-separated string empty_vars_str = ", ".join(empty_variables) logger.error(f"Missing variables: {empty_vars_str}.") - raise ValueError(f"The following variables are empty: {empty_vars_str}.") + raise ValueError(f"The following variables are empty: {empty_vars_str}.") \ No newline at end of file diff --git a/hdforce/Classes.py b/hdforce/Classes.py new file mode 100644 index 0000000..a1ccf89 --- /dev/null +++ b/hdforce/Classes.py @@ -0,0 +1,58 @@ +from typing import List, Dict +from pydantic import BaseModel + +# Athlete Class +class NewAthlete(BaseModel): + # Required + name: str + active: bool = True + # Optional + teams: List[str] = [] + groups: List[str] = [] + external: Dict = {} + +# -------------------- # +# Athlete Class +class Athlete(BaseModel): + # Required + id: str + name: str + active: bool = True + # Optional + teams: List[str] = [] + groups: List[str] = [] + external: Dict = {} + +# -------------------- # +# Team Class +class Team(BaseModel): + name: str + id: str + +# -------------------- # +# Group Class +class Group(BaseModel): + name: str + id: str + +# -------------------- # +# Test Type Class +class TestType(BaseModel): + name: str + id: str + +# -------------------- # +# Tags Class +class Tag(BaseModel): + name: str + id: str + description: str + +# -------------------- # +# AthleteResult Class +# AthleteResult Class +class AthleteResult(BaseModel): + name: str + successful: bool + id: str + reason: List[str] = [] \ No newline at end of file diff --git a/hdforce/CreateAthletes.py b/hdforce/CreateAthletes.py new file mode 100644 index 0000000..2f498a2 --- /dev/null +++ b/hdforce/CreateAthletes.py @@ -0,0 +1,130 @@ +# Dependencies ----- +import requests +import os +import datetime +from typing import List, Dict, Optional +from pydantic import BaseModel +# Package imports +from .AuthManager import AuthManager +from .utils import ConfigManager +from .LoggerConfig import LoggerConfig +from .Classes import NewAthlete, AthleteResult + +# Get a logger specific to this module +logger = LoggerConfig.get_logger(__name__) + +# -------------------- # +# Create Athletes + +def CreateAthletes(athletes: List[NewAthlete]) -> List[AthleteResult]: + """Create athletes for your account. Up to 500 at one time. + + Parameters + ---------- + athletes : list[Athlete] + A list of Athletes with class of `NewAthlete`. + + Returns + ------- + list[AthleteResult] + A list of AthleteResult objects indicating the success or failure of each athlete creation. + + Raises + ------ + Exception + If the HTTP response status is not 200, indicating an unsuccessful API request, or if there is a failure in parsing the JSON response. + """ + # Retrieve Access Token and check expiration + a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") + tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) + + # get current time in timestamp + now = datetime.datetime.now() + nowtime = datetime.datetime.timestamp(now) + + # Validate refresh token and expiration + if a_token is None: + logger.error("No Access Token found.") + raise Exception("No Access Token found.") + elif int(nowtime) >= tokenExp: + logger.debug(f"Token Expired: {datetime.datetime.fromtimestamp(tokenExp)}") + # authenticate + try: + AuthManager( + region=ConfigManager.region, + authMethod=ConfigManager.env_method, + refreshToken_name=ConfigManager.token_name, + refreshToken=ConfigManager.refresh_token, + env_file_name=ConfigManager.file_name + ) + # Retrieve Access Token and check expiration + a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") + logger.debug("New ACCESS_TOKEN retrieved") + tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) + logger.debug("TOKEN_EXPIRATION retrieved") + if a_token is None: + logger.error("No Access Token found.") + raise Exception("No Access Token found.") + elif int(nowtime) >= tokenExp: + logger.debug(f"Token Expired: {datetime.datetime.fromtimestamp(tokenExp)}") + raise Exception("Token expired") + else: + logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") + pass + except ValueError: + logger.error("Failed to authenticate. Try AuthManager") + raise Exception("Failed to authenticate. Try AuthManage") + else: + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") + + # API Cloud URL + url_cloud = os.getenv("CLOUD_URL") + + # GET Request + headers = {"Authorization": f"Bearer {a_token}"} + + # Determine URL and payload based on the number of athletes + url = f"{url_cloud}/athletes/bulk" + payload = [athlete.model_dump() for athlete in athletes] + + # Log the payload being sent + logger.debug(f"Payload being sent to API: {payload}") + + # GET Request + response = requests.post(url, headers=headers, json=payload) + + # Response Handling + if response.status_code != 200: + logger.error(f"Error {response.status_code}: {response.reason}") + raise Exception(f"Error {response.status_code}: {response.reason}") + + try: + response_data = response.json() + data = response_data.get('data', []) + failures = response_data.get('failures', []) + + # Successful athlete names + successful_names = [athlete['name'] for athlete in data] + + # Process failures into a dictionary of reason to list of names + failure_reasons = {} + for failure in failures: + reason = failure['reason'] + name = failure['data'].get('name') + if reason in failure_reasons: + failure_reasons[reason].append(name) + else: + failure_reasons[reason] = [name] + + # Log the successful athletes count + successful_count = len(successful_names) + logger.info(f"Request successful. Athletes created: {successful_count}") + + return { + "successful": successful_names, + "failures": failure_reasons + } + + except ValueError: + logger.error("Failed to parse response JSON") + raise Exception("Failed to parse response JSON") \ No newline at end of file diff --git a/hdforce/GetAthletes.py b/hdforce/GetAthletes.py index 9883bd7..844a80d 100644 --- a/hdforce/GetAthletes.py +++ b/hdforce/GetAthletes.py @@ -15,12 +15,12 @@ # Get Athletes -def GetAthletes(inactive: bool = False) -> pd.DataFrame: +def GetAthletes(includeInactive: bool = False) -> pd.DataFrame: """Get the athlete information from an account. Parameters ---------- - inactive : bool, optional + includeInactive : bool, optional A boolean that specifies whether to include inactive athletes in the results. Default is False, meaning by default inactive athletes are not included. Returns @@ -42,13 +42,10 @@ def GetAthletes(inactive: bool = False) -> pd.DataFrame: # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -83,23 +80,24 @@ def GetAthletes(inactive: bool = False) -> pd.DataFrame: logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # API Cloud URL url_cloud = os.getenv("CLOUD_URL") # Create URL for request - url = f"{url_cloud}/athletes?inactive={inactive}" + url = f"{url_cloud}/athletes?inactive={includeInactive}" # GET Request headers = {"Authorization": f"Bearer {a_token}"} # Create Response - response = requests.get(url, headers=headers) - if inactive: + if includeInactive: logger.debug("GET Request: Athletes (inactive = true)") else: logger.debug("GET Request: Athletes (inactive = false)") + # GET Request + response = requests.get(url, headers=headers) # Response Handling # If Error show error @@ -121,4 +119,4 @@ def GetAthletes(inactive: bool = False) -> pd.DataFrame: # Bad parse or none returned except ValueError: logger.error("Failed to parse JSON response or no data returned.") - raise Exception("Failed to parse JSON response or no data returned.") + raise Exception("Failed to parse JSON response or no data returned.") \ No newline at end of file diff --git a/hdforce/GetForceTime.py b/hdforce/GetForceTime.py index f142fbd..c4afd4f 100644 --- a/hdforce/GetForceTime.py +++ b/hdforce/GetForceTime.py @@ -43,13 +43,10 @@ def GetForceTime(testId: str) -> pd.DataFrame: # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -84,7 +81,7 @@ def GetForceTime(testId: str) -> pd.DataFrame: logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # API Cloud URL url_cloud = os.getenv("CLOUD_URL") @@ -100,9 +97,9 @@ def GetForceTime(testId: str) -> pd.DataFrame: url = f"{url_cloud}/forcetime/{tid}" # GET Request + logger.debug(f"GET Force-Time data for test: {tid}") headers = {"Authorization": f"Bearer {a_token}"} response = requests.get(url, headers=headers) - logger.debug(f"GET Force-Time data for test: {tid}") # Check response status and handle data accordingly if response.status_code != 200: @@ -137,4 +134,4 @@ def GetForceTime(testId: str) -> pd.DataFrame: except ValueError: logger.error("Failed to parse JSON response or no data returned.") - raise Exception("Failed to parse JSON response or no data returned.") + raise Exception("Failed to parse JSON response or no data returned.") \ No newline at end of file diff --git a/hdforce/GetGroups.py b/hdforce/GetGroups.py index be1e859..7552cd4 100644 --- a/hdforce/GetGroups.py +++ b/hdforce/GetGroups.py @@ -29,13 +29,10 @@ def GetGroups() -> pd.DataFrame: # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -70,7 +67,7 @@ def GetGroups() -> pd.DataFrame: logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # API Cloud URL url_cloud = os.getenv("CLOUD_URL") @@ -82,8 +79,8 @@ def GetGroups() -> pd.DataFrame: headers = {"Authorization": f"Bearer {a_token}"} # Create Response - response = requests.get(url, headers=headers) logger.debug("GET Request: Groups") + response = requests.get(url, headers=headers) # Response Handling # If Error show error @@ -104,4 +101,4 @@ def GetGroups() -> pd.DataFrame: except ValueError: logger.error("Failed to parse JSON response or no data returned.") - raise Exception("Failed to parse JSON response or no data returned.") + raise Exception("Failed to parse JSON response or no data returned.") \ No newline at end of file diff --git a/hdforce/GetMetrics.py b/hdforce/GetMetrics.py index 3cd00d9..1a00b6c 100644 --- a/hdforce/GetMetrics.py +++ b/hdforce/GetMetrics.py @@ -34,13 +34,10 @@ def GetMetrics() -> pd.DataFrame: # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -75,7 +72,7 @@ def GetMetrics() -> pd.DataFrame: logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # API Cloud URL url_cloud = os.getenv("CLOUD_URL") @@ -87,8 +84,8 @@ def GetMetrics() -> pd.DataFrame: headers = {"Authorization": f"Bearer {a_token}"} # Create Response - response = requests.get(url, headers=headers) logger.debug("GET Request: Metrics.") + response = requests.get(url, headers=headers) # Response Handling # If Error show error @@ -115,4 +112,4 @@ def GetMetrics() -> pd.DataFrame: except ValueError: logger.error("Failed to parse JSON response or no data returned") - raise Exception("Failed to parse JSON response or no data returned.") + raise Exception("Failed to parse JSON response or no data returned.") \ No newline at end of file diff --git a/hdforce/GetTags.py b/hdforce/GetTags.py index eefdeb3..7cce3f4 100644 --- a/hdforce/GetTags.py +++ b/hdforce/GetTags.py @@ -31,13 +31,10 @@ def GetTags() -> pd.DataFrame: # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -72,7 +69,7 @@ def GetTags() -> pd.DataFrame: logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # API Cloud URL url_cloud = os.getenv("CLOUD_URL") @@ -81,9 +78,9 @@ def GetTags() -> pd.DataFrame: url = f"{url_cloud}/tags" # GET Request + logger.debug("GET Request: Tags") headers = {"Authorization": f"Bearer {a_token}"} response = requests.get(url, headers=headers) - logger.debug("GET Request: Tags") # Response Handling # If Error show error @@ -105,4 +102,4 @@ def GetTags() -> pd.DataFrame: # Bad parse or none returned except ValueError: logger.error("Failed to parse JSON response or no data returned") - raise Exception("Failed to parse JSON response or no data returned.") + raise Exception("Failed to parse JSON response or no data returned.") \ No newline at end of file diff --git a/hdforce/GetTeams.py b/hdforce/GetTeams.py index 508cb44..545a529 100644 --- a/hdforce/GetTeams.py +++ b/hdforce/GetTeams.py @@ -30,13 +30,10 @@ def GetTeams() -> pd.DataFrame: # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -71,7 +68,7 @@ def GetTeams() -> pd.DataFrame: logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # API Cloud URL url_cloud = os.getenv("CLOUD_URL") @@ -80,9 +77,9 @@ def GetTeams() -> pd.DataFrame: url = f"{url_cloud}/teams" # GET Request + logger.debug("GET Request: Teams.") headers = {"Authorization": f"Bearer {a_token}"} response = requests.get(url, headers=headers) - logger.debug("GET Request: Teams.") # Response Handling # If Error show error @@ -105,4 +102,4 @@ def GetTeams() -> pd.DataFrame: # Bad parse or none returned except ValueError: logger.error("Failed to parse JSON response or no data returned.") - raise Exception("Failed to parse JSON response or no data returned.") + raise Exception("Failed to parse JSON response or no data returned.") \ No newline at end of file diff --git a/hdforce/GetTests.py b/hdforce/GetTests.py index 01c0bfe..bc11f83 100644 --- a/hdforce/GetTests.py +++ b/hdforce/GetTests.py @@ -11,7 +11,7 @@ # Get All Tests -def GetTests(from_=None, to_=None, sync=False, active=True) -> pd.DataFrame: +def GetTests(from_=None, to_=None, sync=False, athleteId=None, typeId=None, teamId=None, groupId=None, includeInactive = False) -> pd.DataFrame: """Get all test trials from an account. Allows filtering of results based on time frames, synchronization needs, and the active status of tests. Parameters @@ -25,8 +25,20 @@ def GetTests(from_=None, to_=None, sync=False, active=True) -> pd.DataFrame: sync : bool, optional If True, the function fetches updated and newly created tests to synchronize with the database. Default is False. - active : bool, optional - If True, only active tests are fetched. If False, all tests including inactive ones are fetched. Default is True. + athleteId : str optional + The unique identifier of the athlete whose tests are to be retrieved. + + typeId : str optional + The canonical test ID, test type name, or test name abbreviation. Must correspond to known test types. + + teamId : str optional + A single team ID, tuple or list of team IDs to receive tests from specific teams. + + groupId : str optional + A single group ID or a comma-separated string of group IDs to receive tests from specific groups. + + includeInactive : bool, optional + Default to False, where only active tests are returned. If True, all tests including inactive ones are returned. Returns ------- @@ -46,13 +58,10 @@ def GetTests(from_=None, to_=None, sync=False, active=True) -> pd.DataFrame: # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -87,40 +96,98 @@ def GetTests(from_=None, to_=None, sync=False, active=True) -> pd.DataFrame: logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") - # API Cloud URL - url_cloud = os.getenv("CLOUD_URL") - - # From DateTime - from_dt = "" - if from_ is not None: - if sync: - from_dt = f"?syncFrom={from_}" + # Create URL for request + url = os.getenv("CLOUD_URL") + + # Create blank Query list to handle parameters + query = {} + + # Check if more than one of the specified parameters is provided + provided_params = [athleteId, typeId, teamId, groupId] + provided_count = sum(1 for param in provided_params if param is not None) + # Error if more than one key parameter provided + if provided_count > 1: + raise ValueError("Only one of athleteId, typeId, teamId, or groupId can be provided at the same time.") + + # Evaluate from and to dates for Sync command + if sync is True: + if from_ is not None: + query['syncFrom'] = from_ + if to_ is not None: + query['syncTo'] = to_ + elif sync is False: + if from_ is not None: + query['from'] = from_ + if to_ is not None: + query['to'] = to_ + + # Evaluate for Athlete argument + if athleteId is not None: + query['athleteId'] = athleteId + + # Evaluate for Test Type argument + if typeId is not None: + # Check typeId + type_ids = { + "7nNduHeM5zETPjHxvm7s": ["7nNduHeM5zETPjHxvm7s", "Countermovement Jump", "CMJ"], + "QEG7m7DhYsD6BrcQ8pic": ["QEG7m7DhYsD6BrcQ8pic", "Squat Jump", "SJ"], + "2uS5XD5kXmWgIZ5HhQ3A": ["2uS5XD5kXmWgIZ5HhQ3A", "Isometric Test", "ISO"], + "gyBETpRXpdr63Ab2E0V8": ["gyBETpRXpdr63Ab2E0V8", "Drop Jump", "DJ"], + "5pRSUQVSJVnxijpPMck3": ["5pRSUQVSJVnxijpPMck3", "Free Run", "FREE"], + "pqgf2TPUOQOQs6r0HQWb": ["pqgf2TPUOQOQs6r0HQWb", "CMJ Rebound", "CMJR"], + "r4fhrkPdYlLxYQxEeM78": ["r4fhrkPdYlLxYQxEeM78", "Multi Rebound", "MR"], + "ubeWMPN1lJFbuQbAM97s": ["ubeWMPN1lJFbuQbAM97s", "Weigh In", "WI"], + "rKgI4y3ItTAzUekTUpvR": ["rKgI4y3ItTAzUekTUpvR", "Drop Landing", "DL"], + "4KlQgKmBxbOY6uKTLDFL": ["4KlQgKmBxbOY6uKTLDFL", "TS Free Run", "TSFR"], + "umnEZPgi6zaxuw0KhUpM": ["umnEZPgi6zaxuw0KhUpM", "TS Isometric Test", "TSISO"] + } + # Sort test type Id + for key, values in type_ids.items(): + if typeId in values: + t_id = key + break else: - from_dt = f"?from={from_}" - - # To DateTime - to_dt = "" - if to_ is not None: - if from_ is None: - to_dt = f"?{'syncTo' if sync else 'to'}={to_}" + logger.error("typeId incorrect. Check your entry") + raise Exception("typeId incorrect. Check your entry") + + # Add Test Type Id to params query + query['testTypeId'] = t_id + + # Evaluate for Team Id argument + if teamId is not None: + # Handling teamId input whether single ID or tuple of IDs + if isinstance(teamId, (tuple, list)): + query['teamId'] = ','.join(map(str, teamId)) # Join multiple team IDs into a comma-separated string + elif isinstance(teamId, str): + query['teamId'] = teamId # Use the single team ID as is else: - to_dt = f"&{'syncTo' if sync else 'to'}={to_}" + logger.error("teamId must be a string or a tuple/list of strings.") + raise ValueError("teamId must be a string or a tuple/list of strings.") - # Create URL for request - url = f"{url_cloud}{from_dt}{to_dt}" - # GET Request - headers = {"Authorization": f"Bearer {a_token}"} - response = requests.get(url, headers=headers) + # Evaluate for Group Id argument + if groupId is not None: + # Handling groupId input whether single ID or tuple of IDs + if isinstance(groupId, (tuple, list)): + query['groupId'] = ','.join(map(str, groupId)) # Join multiple team IDs into a comma-separated string + elif isinstance(groupId, str): + query['groupId'] = groupId # Use the single team ID as is + else: + logger.error("groupId must be a string or a tuple/list of strings.") + raise ValueError("groupId must be a string or a tuple/list of strings.") + # Log request - if from_dt is not None and to_dt is not None: + if from_ is not None and to_ is not None: logger.debug(f"Test Request from_dt to_dt") - elif from_dt is None: + elif from_ is None: logger.debug(f"Test Request to_dt") - elif to_dt is None: + elif to_ is None: logger.debug(f"Test Request from_dt") + # GET Request + headers = {"Authorization": f"Bearer {a_token}"} + response = requests.get(url, headers=headers, params=query) # Check response status and handle data accordingly if response.status_code != 200: @@ -138,10 +205,30 @@ def GetTests(from_=None, to_=None, sync=False, active=True) -> pd.DataFrame: df = responseHandler(data) # Filter active tests if required - if 'active' in df.columns and active: + if 'active' in df.columns and includeInactive == False: df = df[df['active'] == True] # Setting attributes + # Create Test Type info for df attrs + if typeId: + if t_id in type_ids: + type_info = type_ids[t_id] + + # Create team info for df attrs + if teamId: + df.attrs['Team Id'] = teamId + + # Create group info for df attrs + if groupId: + df.attrs['Group Id'] = groupId + + # Athlete Real Name + if athleteId: + aName = df['athlete_name'].unique() + aName = str(aName[0]) + df.attrs['Athlete Id'] = athleteId + df.attrs['Athlete Name'] = aName + df.attrs['Last Sync'] = int(data['lastSyncTime']) df.attrs['Last Test Time'] = int(data['lastTestTime']) df.attrs['Count'] = int(data['count']) @@ -155,4 +242,4 @@ def GetTests(from_=None, to_=None, sync=False, active=True) -> pd.DataFrame: return f"JSON Error: {e}" except Exception as e: - return f"An error occurred: {e}" + return f"An error occurred: {e}" \ No newline at end of file diff --git a/hdforce/GetTestsAth.py b/hdforce/GetTestsAth.py index 287f9d1..f248985 100644 --- a/hdforce/GetTestsAth.py +++ b/hdforce/GetTestsAth.py @@ -4,14 +4,17 @@ import datetime import pandas as pd # Package imports -from .utils import responseHandler, logger, ConfigManager +from .utils import responseHandler, logger, ConfigManager, deprecated from .AuthManager import AuthManager +# Enable deprecation warnings globally +import warnings +warnings.simplefilter('always', DeprecationWarning) # -------------------- # # Tests by Athlete - -def GetTestsAth(athleteId: str, from_: int = None, to_: int = None, sync: bool = False, active: bool = True) -> pd.DataFrame: +@deprecated('Use `GetTests` instead, which has been expanded to handle all requests.') +def GetTestsAth(athleteId: str, from_: int = None, to_: int = None, sync: bool = False, includeInactive: bool = False) -> pd.DataFrame: """Get test trials for a specified athlete from an API. The function allows filtering of results based on time frames and the state of the test (active or not). Parameters @@ -28,8 +31,8 @@ def GetTestsAth(athleteId: str, from_: int = None, to_: int = None, sync: bool = sync : bool, optional If True, the function fetches updated and newly created tests to synchronize with the Hawkin database. Default is False. - active : bool, optional - If True, only active tests are fetched. If False, all tests including inactive ones are fetched. Default is True. + includeInactive : bool, optional + Default to False, where only active tests are returned. If True, all tests including inactive ones are returned. Returns ------- @@ -47,17 +50,18 @@ def GetTestsAth(athleteId: str, from_: int = None, to_: int = None, sync: bool = If the HTTP response status is not 200, indicating an unsuccessful API request, or if there is a failure in parsing the JSON response. ValueError If the 'athleteId' parameter is not a string. + + Deprecated + ---------- + Use `GetTests` instead, which has been expanded to handle all requests. """ # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -92,48 +96,45 @@ def GetTestsAth(athleteId: str, from_: int = None, to_: int = None, sync: bool = logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") - - # API Cloud URL - url_cloud = os.getenv("CLOUD_URL") - - # From DateTime - from_dt = "" - if from_ is not None: - if sync: - from_dt = f"&syncFrom={from_}" - else: - from_dt = f"&from={from_}" - - # To DateTime - to_dt = "" - if to_ is not None: - if sync: - to_dt = f"&syncTo={to_}" - else: - to_dt = f"&to={to_}" + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") + + # Create URL for request + url = os.getenv("CLOUD_URL") + + # Create blank Query list to handle parameters + query = {} # Athlete ID if isinstance(athleteId, str): - a_id = athleteId + query['athleteId'] = athleteId else: logger.error("athleteId incorrect. Check your entry") raise Exception("athleteId incorrect. Check your entry") - # Create URL for request - url = f"{url_cloud}?athleteId={a_id}{from_dt}{to_dt}" + # Evaluate from and to dates for Sync command + if sync is True: + if from_ is not None: + query['syncFrom'] = from_ + if to_ is not None: + query['syncTo'] = to_ + elif sync is False: + if from_ is not None: + query['from'] = from_ + if to_ is not None: + query['to'] = to_ - # GET Request - headers = {"Authorization": f"Bearer {a_token}"} - response = requests.get(url, headers=headers) # Log request - if from_dt is not None and to_dt is not None: + if from_ is not None and to_ is not None: logger.debug(f"Athlete Test Request from_dt to_dt") - elif from_dt is None: + elif from_ is None: logger.debug(f"Athlete Test Request to_dt") - elif to_dt is None: + elif to_ is None: logger.debug(f"Athlete Test Request from_dt") + # GET Request + headers = {"Authorization": f"Bearer {a_token}"} + response = requests.get(url, headers=headers, params=query) + # Check response status and handle data accordingly if response.status_code != 200: logger.error(f"Error {response.status_code}: {response.reason}") @@ -150,7 +151,7 @@ def GetTestsAth(athleteId: str, from_: int = None, to_: int = None, sync: bool = df = responseHandler(data) # Filter active tests if required - if 'active' in df.columns and active: + if 'active' in df.columns and includeInactive == False: df = df[df['active'] == True] # Athlete Real Name @@ -158,12 +159,12 @@ def GetTestsAth(athleteId: str, from_: int = None, to_: int = None, sync: bool = aName = str(aName[0]) # Setting attributes - df.attrs['Athlete Id'] = a_id + df.attrs['Athlete Id'] = athleteId df.attrs['Athlete Name'] = aName df.attrs['Last Sync'] = int(data['lastSyncTime']) df.attrs['Last Test Time'] = int(data['lastTestTime']) df.attrs['Count'] = int(data['count']) - logger.info(f"Request successful. Returned {df.attrs['Count']} tests from athlete: {a_id}.") + logger.info(f"Request successful. Returned {df.attrs['Count']} tests from athlete: {athleteId}.") return df except requests.RequestException as e: @@ -173,4 +174,4 @@ def GetTestsAth(athleteId: str, from_: int = None, to_: int = None, sync: bool = return f"JSON Error: {e}" except Exception as e: - return f"An error occurred: {e}" + return f"An error occurred: {e}" \ No newline at end of file diff --git a/hdforce/GetTestsGroup.py b/hdforce/GetTestsGroup.py index 32e59ce..3a446de 100644 --- a/hdforce/GetTestsGroup.py +++ b/hdforce/GetTestsGroup.py @@ -4,14 +4,17 @@ import datetime import pandas as pd # Package imports -from .utils import responseHandler, logger, ConfigManager +from .utils import responseHandler, logger, ConfigManager, deprecated from .AuthManager import AuthManager +# Enable deprecation warnings globally +import warnings +warnings.simplefilter('always', DeprecationWarning) # -------------------- # # Tests by Group - -def GetTestsGroup(groupId: str, from_: int = None, to_: int = None, sync: bool = False, active: bool = True) -> pd.DataFrame: +@deprecated('Use `GetTests` instead, which has been expanded to handle all requests.') +def GetTestsGroup(groupId: str, from_: int = None, to_: int = None, sync: bool = False, includeInactive: bool = False) -> pd.DataFrame: """Get test trials for specified team(s). Allows filtering of results based on time frames, synchronization needs, and the active status of tests. Parameters @@ -28,8 +31,8 @@ def GetTestsGroup(groupId: str, from_: int = None, to_: int = None, sync: bool = sync : bool, optional If True, the function fetches updated and newly created tests to synchronize with the database. Default is False. - active : bool, optional - If True, only active tests are fetched. If False, all tests including inactive ones are fetched. Default is True. + includeInactive : bool, optional + Default to False, where only active tests are returned. If True, all tests including inactive ones are returned. Returns ------- @@ -46,17 +49,18 @@ def GetTestsGroup(groupId: str, from_: int = None, to_: int = None, sync: bool = If the HTTP response status is not 200, indicating an unsuccessful API request, or if there is a failure in parsing the JSON response. ValueError If the 'groupId' parameter is not properly formatted as a string or a list/tuple of strings. + + Deprecated + ---------- + Use `GetTests` instead, which has been expanded to handle all requests. """ # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -91,47 +95,46 @@ def GetTestsGroup(groupId: str, from_: int = None, to_: int = None, sync: bool = logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") - - # API Cloud URL - url_cloud = os.getenv("CLOUD_URL") - - # From DateTime - from_dt = "" - if from_ is not None: - if sync: - from_dt = f"&syncFrom={from_}" - else: - from_dt = f"&from={from_}" - - # To DateTime - to_dt = "" - if to_ is not None: - if sync: - to_dt = f"&syncTo={to_}" - else: - to_dt = f"&to={to_}" - - # Group ID - if isinstance(groupId, str): - g_id = groupId - else: - logger.error("groupId incorrect. Check your entry") - raise Exception("groupId incorrect. Check your entry") + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # Create URL for request - url = f"{url_cloud}?groupId={g_id}{from_dt}{to_dt}" + url = os.getenv("CLOUD_URL") + + # Create blank Query list to handle parameters + query = {} + + # Handling groupId input whether single ID or tuple of IDs + if isinstance(groupId, (tuple, list)): + query['groupId'] = ','.join(map(str, groupId)) # Join multiple team IDs into a comma-separated string + elif isinstance(groupId, str): + query['groupId'] = groupId # Use the single team ID as is + else: + logger.error("groupId must be a string or a tuple/list of strings.") + raise ValueError("groupId must be a string or a tuple/list of strings.") + + # Evaluate from and to dates for Sync command + if sync is True: + if from_ is not None: + query['syncFrom'] = from_ + if to_ is not None: + query['syncTo'] = to_ + elif sync is False: + if from_ is not None: + query['from'] = from_ + if to_ is not None: + query['to'] = to_ - # GET Request - headers = {"Authorization": f"Bearer {a_token}"} - response = requests.get(url, headers=headers) # Log request - if from_dt is not None and to_dt is not None: + if from_ is not None and to_ is not None: logger.debug(f"Group Test Request from_dt to_dt") - elif from_dt is None: + elif from_ is None: logger.debug(f"Group Test Request to_dt") - elif to_dt is None: + elif to_ is None: logger.debug(f"Group Test Request from_dt") + + # GET Request + headers = {"Authorization": f"Bearer {a_token}"} + response = requests.get(url, headers=headers, params=query) # Check response status and handle data accordingly if response.status_code != 200: @@ -149,7 +152,7 @@ def GetTestsGroup(groupId: str, from_: int = None, to_: int = None, sync: bool = df = responseHandler(data) # Filter active tests if required - if 'active' in df.columns and active: + if 'active' in df.columns and includeInactive == False: df = df[df['active'] == True] # Setting attributes @@ -167,4 +170,4 @@ def GetTestsGroup(groupId: str, from_: int = None, to_: int = None, sync: bool = return f"JSON Error: {e}" except Exception as e: - return f"An error occurred: {e}" + return f"An error occurred: {e}" \ No newline at end of file diff --git a/hdforce/GetTestsTeam.py b/hdforce/GetTestsTeam.py index 7776599..21129e2 100644 --- a/hdforce/GetTestsTeam.py +++ b/hdforce/GetTestsTeam.py @@ -4,14 +4,16 @@ import datetime import pandas as pd # Package imports -from .utils import responseHandler, logger, ConfigManager +from .utils import responseHandler, logger, ConfigManager, deprecated from .AuthManager import AuthManager - +# Enable deprecation warnings globally +import warnings +warnings.simplefilter('always', DeprecationWarning) # -------------------- # # Tests by Team - -def GetTestsTeam(teamId: str, from_: int = None, to_: int = None, sync: bool = False, active: bool = True) -> pd.DataFrame: +@deprecated('Use `GetTests` instead, which has been expanded to handle all requests.') +def GetTestsTeam(teamId: str, from_: int = None, to_: int = None, sync: bool = False, includeInactive: bool = False) -> pd.DataFrame: """Get test trials for specified team(s). Allows filtering of results based on time frames, synchronization needs, and the active status of tests. Parameters @@ -28,8 +30,8 @@ def GetTestsTeam(teamId: str, from_: int = None, to_: int = None, sync: bool = F sync : bool, optional If True, the function fetches updated and newly created tests to synchronize with the database. Default is False. - active : bool, optional - If True, only active tests are fetched. If False, all tests including inactive ones are fetched. Default is True. + includeInactive : bool, optional + Default to False, where only active tests are returned. If True, all tests including inactive ones are returned. Returns ------- @@ -46,17 +48,18 @@ def GetTestsTeam(teamId: str, from_: int = None, to_: int = None, sync: bool = F If the HTTP response status is not 200, indicating an unsuccessful API request, or if there is a failure in parsing the JSON response. ValueError If the 'teamId' parameter is not properly formatted as a string or a list/tuple of strings. + + Deprecated + ---------- + Use `GetTests` instead, which has been expanded to handle all requests. """ # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -91,39 +94,46 @@ def GetTestsTeam(teamId: str, from_: int = None, to_: int = None, sync: bool = F logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # API Cloud URL - url_cloud = os.getenv("CLOUD_URL") - - # From datetime - from_dt = f"&syncFrom={from_}" if from_ is not None and sync else f"&from={from_}" if from_ is not None else "" + url = os.getenv("CLOUD_URL") - # To datetime - to_dt = f"&syncTo={to_}" if to_ is not None and sync else f"&to={to_}" if to_ is not None else "" + # Create blank Query list to handle parameters + query = {} # Handling teamId input whether single ID or tuple of IDs if isinstance(teamId, (tuple, list)): - t_id = ','.join(map(str, teamId)) # Join multiple team IDs into a comma-separated string + query['teamId'] = ','.join(map(str, teamId)) # Join multiple team IDs into a comma-separated string elif isinstance(teamId, str): - t_id = teamId # Use the single team ID as is + query['teamId'] = teamId # Use the single team ID as is else: logger.error("teamId must be a string or a tuple/list of strings.") raise ValueError("teamId must be a string or a tuple/list of strings.") - # Create URL for request - url = f"{url_cloud}?teamId={t_id}{from_dt}{to_dt}" + # Evaluate from and to dates for Sync command + if sync is True: + if from_ is not None: + query['syncFrom'] = from_ + if to_ is not None: + query['syncTo'] = to_ + elif sync is False: + if from_ is not None: + query['from'] = from_ + if to_ is not None: + query['to'] = to_ - # GET Request - headers = {"Authorization": f"Bearer {a_token}"} - response = requests.get(url, headers=headers) # Log request - if from_dt is not None and to_dt is not None: + if from_ is not None and to_ is not None: logger.debug(f"Team Test Request from_dt to_dt") - elif from_dt is None: + elif from_ is None: logger.debug(f"Team Test Request to_dt") - elif to_dt is None: + elif to_ is None: logger.debug(f"Team Test Request from_dt") + + # GET Request + headers = {"Authorization": f"Bearer {a_token}"} + response = requests.get(url, headers=headers, params=query) # Check response status and handle data accordingly if response.status_code != 200: @@ -141,7 +151,7 @@ def GetTestsTeam(teamId: str, from_: int = None, to_: int = None, sync: bool = F df = responseHandler(data) # Filter active tests if required - if 'active' in df.columns and active: + if 'active' in df.columns and includeInactive == False: df = df[df['active'] == True] # Setting attributes @@ -159,4 +169,4 @@ def GetTestsTeam(teamId: str, from_: int = None, to_: int = None, sync: bool = F return f"JSON Error: {e}" except Exception as e: - return f"An error occurred: {e}" + return f"An error occurred: {e}" \ No newline at end of file diff --git a/hdforce/GetTestsType.py b/hdforce/GetTestsType.py index de31f2b..198ef76 100644 --- a/hdforce/GetTestsType.py +++ b/hdforce/GetTestsType.py @@ -4,14 +4,17 @@ import datetime import pandas as pd # Package imports -from .utils import responseHandler, logger, ConfigManager +from .utils import responseHandler, logger, ConfigManager, deprecated from .AuthManager import AuthManager +# Enable deprecation warnings globally +import warnings +warnings.simplefilter('always', DeprecationWarning) # -------------------- # # Tests by Type - -def GetTestsType(typeId: str, from_: int = None, to_: int = None, sync: bool = False, active: bool = True) -> pd.DataFrame: +@deprecated('Use `GetTests` instead, which has been expanded to handle all requests.') +def GetTestsType(typeId: str, from_: int = None, to_: int = None, sync: bool = False, includeInactive: bool = False) -> pd.DataFrame: """Get tests trials based on a specific test type from an API. Allows filtering of results based on time frames and the state of the test (active or not). Parameters @@ -28,8 +31,8 @@ def GetTestsType(typeId: str, from_: int = None, to_: int = None, sync: bool = F sync : bool, optional If True, the function fetches updated and newly created tests to synchronize with the database. Default is False. - active : bool, optional - If True, only active tests are fetched. If False, all tests including inactive ones are fetched. Default is True. + includeInactive : bool, optional + Default to False, where only active tests are returned. If True, all tests including inactive ones are returned. Returns ------- @@ -47,17 +50,18 @@ def GetTestsType(typeId: str, from_: int = None, to_: int = None, sync: bool = F If the HTTP response status is not 200, indicating an unsuccessful API request, or if there is a failure in parsing the JSON response. Exception If the provided 'typeId' does not match any known test types, causing an inability to proceed with the API request. + + Deprecated + ---------- + Use `GetTests` instead, which has been expanded to handle all requests. """ # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -92,39 +96,37 @@ def GetTestsType(typeId: str, from_: int = None, to_: int = None, sync: bool = F logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") - - # API Cloud URL - url_cloud = os.getenv("CLOUD_URL") - - # From DateTime - from_dt = "" - if from_ is not None: - if sync: - from_dt = f"&syncFrom={from_}" - else: - from_dt = f"&from={from_}" - - # To DateTime - to_dt = "" - if to_ is not None: - if sync: - to_dt = f"&syncTo={to_}" - else: - to_dt = f"&to={to_}" + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") + + # Create blank Query list to handle parameters + query = {} + + # Evaluate from and to dates for Sync command + if sync is True: + if from_ is not None: + query['syncFrom'] = from_ + if to_ is not None: + query['syncTo'] = to_ + elif sync is False: + if from_ is not None: + query['from'] = from_ + if to_ is not None: + query['to'] = to_ # Check typeId type_ids = { - "7nNduHeM5zETPjHxvm7s": ["7nNduHeM5zETPjHxvm7s", "Countermovement Jump", "CMJ"], - "QEG7m7DhYsD6BrcQ8pic": ["QEG7m7DhYsD6BrcQ8pic", "Squat Jump", "SJ"], - "2uS5XD5kXmWgIZ5HhQ3A": ["2uS5XD5kXmWgIZ5HhQ3A", "Isometric Test", "ISO"], - "gyBETpRXpdr63Ab2E0V8": ["gyBETpRXpdr63Ab2E0V8", "Drop Jump", "DJ"], - "5pRSUQVSJVnxijpPMck3": ["5pRSUQVSJVnxijpPMck3", "Free Run", "FREE"], - "pqgf2TPUOQOQs6r0HQWb": ["pqgf2TPUOQOQs6r0HQWb", "CMJ Rebound", "CMJR"], - "r4fhrkPdYlLxYQxEeM78": ["r4fhrkPdYlLxYQxEeM78", "Multi Rebound", "MR"], - "ubeWMPN1lJFbuQbAM97s": ["ubeWMPN1lJFbuQbAM97s", "Weigh In", "WI"], - "rKgI4y3ItTAzUekTUpvR": ["rKgI4y3ItTAzUekTUpvR", "Drop Landing", "DL"] - } + "7nNduHeM5zETPjHxvm7s": ["7nNduHeM5zETPjHxvm7s", "Countermovement Jump", "CMJ"], + "QEG7m7DhYsD6BrcQ8pic": ["QEG7m7DhYsD6BrcQ8pic", "Squat Jump", "SJ"], + "2uS5XD5kXmWgIZ5HhQ3A": ["2uS5XD5kXmWgIZ5HhQ3A", "Isometric Test", "ISO"], + "gyBETpRXpdr63Ab2E0V8": ["gyBETpRXpdr63Ab2E0V8", "Drop Jump", "DJ"], + "5pRSUQVSJVnxijpPMck3": ["5pRSUQVSJVnxijpPMck3", "Free Run", "FREE"], + "pqgf2TPUOQOQs6r0HQWb": ["pqgf2TPUOQOQs6r0HQWb", "CMJ Rebound", "CMJR"], + "r4fhrkPdYlLxYQxEeM78": ["r4fhrkPdYlLxYQxEeM78", "Multi Rebound", "MR"], + "ubeWMPN1lJFbuQbAM97s": ["ubeWMPN1lJFbuQbAM97s", "Weigh In", "WI"], + "rKgI4y3ItTAzUekTUpvR": ["rKgI4y3ItTAzUekTUpvR", "Drop Landing", "DL"], + "4KlQgKmBxbOY6uKTLDFL": ["4KlQgKmBxbOY6uKTLDFL", "TS Free Run", "TSFR"], + "umnEZPgi6zaxuw0KhUpM": ["umnEZPgi6zaxuw0KhUpM", "TS Isometric Test", "TSISO"] + } for key, values in type_ids.items(): if typeId in values: @@ -134,20 +136,24 @@ def GetTestsType(typeId: str, from_: int = None, to_: int = None, sync: bool = F logger.error("typeId incorrect. Check your entry") raise Exception("typeId incorrect. Check your entry") + # Add Test Type Id to params query + query['testTypeId'] = t_id + # Create URL for request - url = f"{url_cloud}?testTypeId={t_id}{from_dt}{to_dt}" + url = os.getenv("CLOUD_URL") - # GET Request - headers = {"Authorization": f"Bearer {a_token}"} - response = requests.get(url, headers=headers) # Log request - if from_dt is not None and to_dt is not None: + if from_ is not None and to_ is not None: logger.debug(f"Test Type Request from_dt to_dt") - elif from_dt is None: + elif from_ is None: logger.debug(f"Test Type Request to_dt") - elif to_dt is None: + elif to_ is None: logger.debug(f"Test Type Request from_dt") + # GET Request + headers = {"Authorization": f"Bearer {a_token}"} + response = requests.get(url, headers=headers, params=query) + # Check response status and handle data accordingly if response.status_code != 200: logger.error(f"Error {response.status_code}: {response.reason}") @@ -164,7 +170,7 @@ def GetTestsType(typeId: str, from_: int = None, to_: int = None, sync: bool = F df = responseHandler(data) # Filter active tests if required - if 'active' in df.columns and active: + if 'active' in df.columns and includeInactive == False: df = df[df['active'] == True] # Create Test Type info for df attrs @@ -187,4 +193,4 @@ def GetTestsType(typeId: str, from_: int = None, to_: int = None, sync: bool = F return f"JSON Error: {e}" except Exception as e: - return f"An error occurred: {e}" + return f"An error occurred: {e}" \ No newline at end of file diff --git a/hdforce/GetTypes.py b/hdforce/GetTypes.py index 200c4ea..f24b82a 100644 --- a/hdforce/GetTypes.py +++ b/hdforce/GetTypes.py @@ -30,13 +30,10 @@ def GetTypes() -> pd.DataFrame: # Retrieve Access Token and check expiration a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) - logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # get current time in timestamp now = datetime.datetime.now() nowtime = datetime.datetime.timestamp(now) - if nowtime < tokenExp: - logger.debug(f"Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") # Validate refresh token and expiration if a_token is None: @@ -71,7 +68,7 @@ def GetTypes() -> pd.DataFrame: logger.error("Failed to authenticate. Try AuthManager") raise Exception("Failed to authenticate. Try AuthManage") else: - logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") # API Cloud URL url_cloud = os.getenv("CLOUD_URL") @@ -83,8 +80,8 @@ def GetTypes() -> pd.DataFrame: headers = {"Authorization": f"Bearer {a_token}"} # Send the GET request to the API - response = requests.get(url, headers=headers) logger.debug("GET request sent for Test Types.") + response = requests.get(url, headers=headers) # Check if the API response was successful if response.status_code != 200: @@ -103,4 +100,4 @@ def GetTypes() -> pd.DataFrame: except ValueError: logger.error("Failed to parse JSON response or no data returned.") - raise Exception("Failed to parse JSON response or no data returned.") + raise Exception("Failed to parse JSON response or no data returned.") \ No newline at end of file diff --git a/hdforce/UpdateAthletes.py b/hdforce/UpdateAthletes.py new file mode 100644 index 0000000..f0cdf52 --- /dev/null +++ b/hdforce/UpdateAthletes.py @@ -0,0 +1,130 @@ +# Dependencies ----- +import requests +import os +import datetime +from typing import List, Dict, Optional +from pydantic import BaseModel +# Package imports +from .AuthManager import AuthManager +from .utils import ConfigManager +from .LoggerConfig import LoggerConfig +from .Classes import Athlete, AthleteResult + +# Get a logger specific to this module +logger = LoggerConfig.get_logger(__name__) + +# -------------------- # +# Update Athletes + +def UpdateAthletes(athletes: List[Athlete]) -> List[AthleteResult]: + """Update athletes for your account. Up to 500 at one time. + + Parameters + ---------- + athletes : list[Athlete] + A list of Athletes with class of `Athlete`. + + Returns + ------- + list[AthleteResult] + A list of AthleteResult objects indicating the success or failure of each athlete creation. + + Raises + ------ + Exception + If the HTTP response status is not 200, indicating an unsuccessful API request, or if there is a failure in parsing the JSON response. + """ + # Retrieve Access Token and check expiration + a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") + tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) + + # get current time in timestamp + now = datetime.datetime.now() + nowtime = datetime.datetime.timestamp(now) + + # Validate refresh token and expiration + if a_token is None: + logger.error("No Access Token found.") + raise Exception("No Access Token found.") + elif int(nowtime) >= tokenExp: + logger.debug(f"Token Expired: {datetime.datetime.fromtimestamp(tokenExp)}") + # authenticate + try: + AuthManager( + region=ConfigManager.region, + authMethod=ConfigManager.env_method, + refreshToken_name=ConfigManager.token_name, + refreshToken=ConfigManager.refresh_token, + env_file_name=ConfigManager.file_name + ) + # Retrieve Access Token and check expiration + a_token = ConfigManager.get_env_variable("ACCESS_TOKEN") + logger.debug("New ACCESS_TOKEN retrieved") + tokenExp = int(ConfigManager.get_env_variable("TOKEN_EXPIRATION")) + logger.debug("TOKEN_EXPIRATION retrieved") + if a_token is None: + logger.error("No Access Token found.") + raise Exception("No Access Token found.") + elif int(nowtime) >= tokenExp: + logger.debug(f"Token Expired: {datetime.datetime.fromtimestamp(tokenExp)}") + raise Exception("Token expired") + else: + logger.debug(f"New Access Token valid through: {datetime.datetime.fromtimestamp(tokenExp)}") + pass + except ValueError: + logger.error("Failed to authenticate. Try AuthManager") + raise Exception("Failed to authenticate. Try AuthManage") + else: + logger.debug(f"Access Token retrieved. expires {datetime.datetime.fromtimestamp(tokenExp)}") + + # API Cloud URL + url_cloud = os.getenv("CLOUD_URL") + + # GET Request + headers = {"Authorization": f"Bearer {a_token}"} + + # Determine URL and payload based on the number of athletes + url = f"{url_cloud}/athletes/bulk" + payload = [athlete.model_dump() for athlete in athletes] + + # Log the payload being sent + logger.debug(f"Payload being sent to API: {payload}") + + # GET Request + response = requests.put(url, headers=headers, json=payload) + + # Response Handling + if response.status_code != 200: + logger.error(f"Error {response.status_code}: {response.reason}") + raise Exception(f"Error {response.status_code}: {response.reason}") + + try: + response_data = response.json() + data = response_data.get('data', []) + failures = response_data.get('failures', []) + + # Successful athlete names + successful_names = [athlete['name'] for athlete in data] + + # Process failures into a dictionary of name to reason + failure_reasons = {failure['data']['name']: failure['reason'] for failure in failures} + + # Create a list of AthleteResult objects + results = [] + for athlete in athletes: + if athlete.name in successful_names: + results.append(AthleteResult(name=athlete.name, id=athlete.id, successful=True, reason=[])) + elif athlete.name in failure_reasons: + results.append(AthleteResult(name=athlete.name, id=athlete.id, successful=False, reason=[failure_reasons[athlete.name]])) + else: + results.append(AthleteResult(name=athlete.name, id=athlete.id, successful=False, reason=["Unknown error"])) + + # Log the successful athletes count + successful_count = sum(result.successful for result in results) + logger.info(f"Request successful. Athletes updated: {successful_count}") + + return results + + except ValueError: + logger.error("Failed to parse JSON response or no data returned.") + raise Exception("Failed to parse JSON response or no data returned.") \ No newline at end of file diff --git a/hdforce/__init__.py b/hdforce/__init__.py index f0b1f25..a572589 100644 --- a/hdforce/__init__.py +++ b/hdforce/__init__.py @@ -18,4 +18,4 @@ from .GetAthletes import GetAthletes from .GetGroups import GetGroups from .GetTeams import GetTeams -from .GetTags import GetTags +from .GetTags import GetTags \ No newline at end of file diff --git a/hdforce/utils.py b/hdforce/utils.py index 1aa4c96..8a92747 100644 --- a/hdforce/utils.py +++ b/hdforce/utils.py @@ -304,3 +304,29 @@ def custom_order(col): df.columns = [col.replace('athlete_external_', 'external_') for col in df.columns] return df + + +# -------------------- # +# Deprecation Decorator + +import warnings +import functools + +def deprecated(reason): + """ + Decorator to mark functions as deprecated. + + Args: + reason (str): The reason why the function is deprecated and what to use instead. + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + f"Function {func.__name__} is deprecated: {reason}", + category=DeprecationWarning, + stacklevel=2 + ) + return func(*args, **kwargs) + return wrapper + return decorator \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index a6e8c8d..1244e35 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: hdforce by Hawkin Dynamics +site_name: hdforce v1.1.2 by Hawkin Dynamics nav: - Home: index.md - User Guide: @@ -7,6 +7,7 @@ nav: - Example Usage: UserGuide/Examples.md - Function: - AuthManager: Functions/AuthManager.md + - CreateAthletes: Functions/CreateAthletes.md - LoggerConfig: Functions/LoggerConfig.md - GetMetrics: Functions/GetMetrics.md - GetTypes: Functions/GetTypes.md @@ -20,6 +21,7 @@ nav: - GetTestsTeams: Functions/GetTestsTeam.md - GetTestsGroups: Functions/GetTestsGroup.md - GetForceTime: Functions/GetForceTime.md + - UpdateAthletes: Functions/UpdateAthletes.md - About: - Changelog: About/changelog.md - Contact: About/contact.md diff --git a/pyproject.toml b/pyproject.toml index 00b28f9..8fd5958 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,15 @@ [tool.poetry] name = "hdforce" -version= "1.0.0" +version= "1.1.2" description = "Get your data from the Hawkin Dynamics API" authors = ["laureng-hd "] readme = "README.md" keywords = ["hawkin", "force plates", "sports science"] homepage = "https://www.hawkindynamics.com/" repository = "https://github.com/HawkinDynamics/hawkinPy" -documentation = "https://silver-adventure-22j19qq.pages.github.io/" +documentation = "https://hawkindynamics.github.io/hawkinPy/" classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Software Development :: Build Tools", "License :: OSI Approved :: MIT License", @@ -27,24 +27,12 @@ python-dotenv = "^1.0.1" requests = "^2.31.0" pytest = "^8.2.0" flake8 = "^7.0.0" +pydantic = "^2.0.0" [tool.poetry.group.dev.dependencies] ipykernel = "^6.29.4" -[tool.poetry-dynamic-versioning] -enable = true -vcs = "git" -bump = true -tag-branch = "main" -pattern = "^(?P\\d+\\.\\d+\\.\\d+)(-?((?P[a-zA-Z]+)\\.?(?P\\d+)?))?" -format-jinja = """ - {%- if distance == 0 -%} - {{- base -}} - {%- else -%} - {{- base }}.dev{{ distance }}+g{{commit}} - {%- endif -%} -""" [build-system] -requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] -build-backend = "poetry_dynamic_versioning.backend" \ No newline at end of file +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/tests/test_CreateAthlete.py b/tests/test_CreateAthlete.py new file mode 100644 index 0000000..e80b57e --- /dev/null +++ b/tests/test_CreateAthlete.py @@ -0,0 +1,126 @@ +import pytest +from unittest.mock import patch, MagicMock +from datetime import datetime +from hdforce.AuthManager import AuthManager +from hdforce.CreateAthletes import CreateAthletes +from hdforce.Classes import NewAthlete, AthleteResult + +# Mocked response generator for successful athlete creation +def mock_success_response(formatted_time): + return { + 'data': [{'name': f'Name_{formatted_time}', 'id': 'athlete_id_1'}], + 'failures': [] + } + +# Mocked response generator for failed athlete creation +def mock_failure_response(formatted_time): + return { + 'data': [], + 'failures': [{'reason': 'Duplicate or Invalid Athlete Name', 'data': {'name': f'Name_{formatted_time}'}}] + } + +# Successful call with file +@patch('hdforce.CreateAthletes.requests.post') +def test_CreateAthletes_file(mock_post): + # Get the current time + current_time = datetime.now() + + # Format the current time as a string + formatted_time = current_time.strftime("%Y%m%d%H%M%S") + + # Mock the POST request response + mock_post.return_value = MagicMock(status_code=200, json=lambda: mock_success_response(formatted_time)) + + # Authenticate + AuthManager(authMethod="file", env_file_name="tests/.env") + + # Create New Athletes + players = [ + NewAthlete(name=f"Name_{formatted_time}", active=False) + ] + + # Create Athlete + response = CreateAthletes(athletes=players) + + # Check response is a dictionary + assert isinstance(response, dict) + assert 'successful' in response + assert 'failures' in response + + # Check successful athletes + assert len(response['successful']) == 1 + assert response['successful'][0] == f"Name_{formatted_time}" + + # Check failures + assert len(response['failures']) == 0 + +# Successful call with env +@patch('hdforce.CreateAthletes.requests.post') +def test_CreateAthletes_env(mock_post): + # Get the current time + current_time = datetime.now() + + # Format the current time as a string + formatted_time = current_time.strftime("%Y%m%d%H%M%S") + + # Mock the POST request response + mock_post.return_value = MagicMock(status_code=200, json=lambda: mock_success_response(formatted_time)) + + # Authenticate + AuthManager() + + # Create New Athletes + players = [ + NewAthlete(name=f"Name_{formatted_time}", active=False) + ] + + # Create Athlete + response = CreateAthletes(athletes=players) + + # Check response is a dictionary + assert isinstance(response, dict) + assert 'successful' in response + assert 'failures' in response + + # Check successful athletes + assert len(response['successful']) == 1 + assert response['successful'][0] == f"Name_{formatted_time}" + + # Check failures + assert len(response['failures']) == 0 + +# Test for failure response +@patch('hdforce.CreateAthletes.requests.post') +def test_CreateAthletes_failure(mock_post): + # Get the current time + current_time = datetime.now() + + # Format the current time as a string + formatted_time = current_time.strftime("%Y%m%d%H%M%S") + + # Mock the POST request response + mock_post.return_value = MagicMock(status_code=200, json=lambda: mock_failure_response(formatted_time)) + + # Authenticate + AuthManager(authMethod="file", env_file_name="tests/.env") + + # Create New Athletes + players = [ + NewAthlete(name=f"Name_{formatted_time}", active=False) + ] + + # Create Athlete + response = CreateAthletes(athletes=players) + + # Check response is a dictionary + assert isinstance(response, dict) + assert 'successful' in response + assert 'failures' in response + + # Check successful athletes + assert len(response['successful']) == 0 + + # Check failures + assert len(response['failures']) == 1 + assert 'Duplicate or Invalid Athlete Name' in response['failures'] + assert response['failures']['Duplicate or Invalid Athlete Name'][0] == f"Name_{formatted_time}" diff --git a/tests/test_GetAthlete.py b/tests/test_GetAthlete.py index d6317bb..5daf306 100644 --- a/tests/test_GetAthlete.py +++ b/tests/test_GetAthlete.py @@ -26,4 +26,4 @@ def test_GetAthletes_env(): # Check response is DataFrame assert isinstance(players, pd.DataFrame) - assert isinstance(players.attrs["Count"], int) + assert isinstance(players.attrs["Count"], int) \ No newline at end of file diff --git a/tests/test_GetGroups.py b/tests/test_GetGroups.py index e500691..c1b2a5a 100644 --- a/tests/test_GetGroups.py +++ b/tests/test_GetGroups.py @@ -26,4 +26,4 @@ def test_GetGroups_env(): # Check response is DataFrame assert isinstance(groups, pd.DataFrame) - assert isinstance(groups.attrs['Count'], int) + assert isinstance(groups.attrs['Count'], int) \ No newline at end of file diff --git a/tests/test_GetMetrics.py b/tests/test_GetMetrics.py index 549044e..4a8bf12 100644 --- a/tests/test_GetMetrics.py +++ b/tests/test_GetMetrics.py @@ -26,4 +26,4 @@ def test_GetMetrics_env(): # Check response is DataFrame assert isinstance(metrics, pd.DataFrame) - assert isinstance(metrics.attrs['Count'], int) + assert isinstance(metrics.attrs['Count'], int) \ No newline at end of file diff --git a/tests/test_GetTests.py b/tests/test_GetTests.py index 336a80f..40dfadb 100644 --- a/tests/test_GetTests.py +++ b/tests/test_GetTests.py @@ -3,6 +3,7 @@ from hdforce.GetTests import GetTests import pandas as pd +#----- Base Call -----# # successful call with file def test_GetTests_file(): @@ -17,7 +18,69 @@ def test_GetTests_file(): assert isinstance(response.attrs['Last Sync'], int) assert isinstance(response.attrs['Last Test Time'], int) +#----- Athlete ID -----# +# successful call with file +def test_GetTests_Ath_file(): + + # Authenticate + AuthManager(authMethod= "file", env_file_name= "tests\.env") + # Call for tests by athlete + response = GetTests(athleteId = "OLbsebtmf81eiwg1AeE5", from_=1690859091, to_=1695688065) + + # Check response is DataFrame + assert isinstance(response, pd.DataFrame) + assert isinstance(response.attrs['Count'], int) + assert isinstance(response.attrs['Last Sync'], int) + assert isinstance(response.attrs['Last Test Time'], int) + +#----- Group ID -----# +# successful call with file +def test_GetTests_Group_file(): + + # Authenticate + AuthManager(authMethod= "file", env_file_name= "tests\.env") + # Call for tests by group + response = GetTests(groupId = "yh8RnOvg56dQNrZGBKWZ", from_= 1690859091, to_= 1700000000) + + # Check response is DataFrame + assert isinstance(response, pd.DataFrame) + assert isinstance(response.attrs['Count'], int) + assert isinstance(response.attrs['Last Sync'], int) + assert isinstance(response.attrs['Last Test Time'], int) + +#----- Team ID -----# +# successful call with file +def test_GetTests_Team_file(): + # Authenticate + AuthManager(authMethod= "file", env_file_name= "tests\.env") + # Call for tests by team + response = GetTests(teamId = "vW9iEKafhs2PamfKSdGC", from_= 1690859091, to_= 1700000000) + + # Check response is DataFrame + assert isinstance(response, pd.DataFrame) + assert isinstance(response.attrs['Count'], int) + assert isinstance(response.attrs['Last Sync'], int) + assert isinstance(response.attrs['Last Test Time'], int) + +#----- Test Type ID -----# +# successful call with file +def test_GetTests_Type_file(): + + # Authenticate + AuthManager(authMethod= "file", env_file_name= "tests\.env") + # Call for tests by type + response = GetTests(typeId = "Countermovement Jump", from_=1690859091, to_=1695688065) + + # Check response is DataFrame + assert isinstance(response, pd.DataFrame) + assert isinstance(response.attrs['Count'], int) + assert isinstance(response.attrs['Last Sync'], int) + assert isinstance(response.attrs['Last Test Time'], int) + +#--------------------------------------------------# + +#----- Base Call -----# # successful call with env def test_GetTests_env(): @@ -31,3 +94,65 @@ def test_GetTests_env(): assert isinstance(response.attrs['Count'], int) assert isinstance(response.attrs['Last Sync'], int) assert isinstance(response.attrs['Last Test Time'], int) + +#----- Athlete ID -----# +# successful call with env +def test_GetTests_Ath_env(): + + # Authenticate + AuthManager() + # Call for tests by athlete + response = GetTests(athleteId = "OLbsebtmf81eiwg1AeE5", from_=1690859091, to_=1695688065) + + # Check response is DataFrame + assert isinstance(response, pd.DataFrame) + assert isinstance(response.attrs['Count'], int) + assert isinstance(response.attrs['Last Sync'], int) + assert isinstance(response.attrs['Last Test Time'], int) + +#----- Group ID -----# +# successful call with env +def test_GetTests_Group_env(): + + # Authenticate + AuthManager() + # Call for tests by group + response = GetTests(groupId = "yh8RnOvg56dQNrZGBKWZ", from_= 1690859091, to_= 1700000000) + + # Check response is DataFrame + assert isinstance(response, pd.DataFrame) + assert isinstance(response.attrs['Count'], int) + assert isinstance(response.attrs['Last Sync'], int) + assert isinstance(response.attrs['Last Test Time'], int) + + +#----- Team ID -----# +# successful call with env +def test_GetTests_Team_env(): + + # Authenticate + AuthManager() + # Call for tests by team + response = GetTests(teamId = "vW9iEKafhs2PamfKSdGC", from_= 1690859091, to_= 1700000000) + + # Check response is DataFrame + assert isinstance(response, pd.DataFrame) + assert isinstance(response.attrs['Count'], int) + assert isinstance(response.attrs['Last Sync'], int) + assert isinstance(response.attrs['Last Test Time'], int) + + +#----- Test Type ID -----# +# successful call with env +def test_GetTests_Type_env(): + + # Authenticate + AuthManager() + # Call for tests by type + response = GetTests(typeId = "Countermovement Jump", from_=1690859091, to_=1695688065) + + # Check response is DataFrame + assert isinstance(response, pd.DataFrame) + assert isinstance(response.attrs['Count'], int) + assert isinstance(response.attrs['Last Sync'], int) + assert isinstance(response.attrs['Last Test Time'], int) \ No newline at end of file diff --git a/tests/test_GetTestsAth.py b/tests/test_GetTestsAth.py index e2231fc..04aaf5e 100644 --- a/tests/test_GetTestsAth.py +++ b/tests/test_GetTestsAth.py @@ -30,4 +30,4 @@ def test_GetTestsAth_env(): assert isinstance(response, pd.DataFrame) assert isinstance(response.attrs['Count'], int) assert isinstance(response.attrs['Last Sync'], int) - assert isinstance(response.attrs['Last Test Time'], int) + assert isinstance(response.attrs['Last Test Time'], int) \ No newline at end of file diff --git a/tests/test_UpdateAthlete.py b/tests/test_UpdateAthlete.py new file mode 100644 index 0000000..0a5851d --- /dev/null +++ b/tests/test_UpdateAthlete.py @@ -0,0 +1,122 @@ +import pytest +from unittest.mock import patch, MagicMock +from datetime import datetime +from hdforce.AuthManager import AuthManager +from hdforce.UpdateAthletes import UpdateAthletes +from hdforce.Classes import Athlete, AthleteResult + +# Mocked response generator for successful athlete update +def mock_success_response(formatted_time): + return { + 'data': [{'name': f'Name_{formatted_time}', 'id': 'athlete_id_1'}], + 'failures': [] + } + +# Mocked response generator for failed athlete update +def mock_failure_response(formatted_time): + return { + 'data': [], + 'failures': [{'reason': 'Duplicate or Invalid Athlete Name', 'data': {'name': f'Name_{formatted_time}', 'id': 'athlete_id_1'}}] + } + +# Successful call with file +@patch('hdforce.UpdateAthletes.requests.put') +def test_UpdateAthletes_file(mock_put): + # Get the current time + current_time = datetime.now() + + # Format the current time as a string + formatted_time = current_time.strftime("%Y%m%d%H%M%S") + + # Mock the PUT request response + mock_put.return_value = MagicMock(status_code=200, json=lambda: mock_success_response(formatted_time)) + + # Authenticate + AuthManager(authMethod="file", env_file_name="tests/.env") + + # Create New Athletes + players = [ + Athlete(id="athlete_id_1", name=f"Name_{formatted_time}", active=True) + ] + + # Update Athlete + response = UpdateAthletes(athletes=players) + + # Check response is a list of AthleteResult + assert isinstance(response, list) + assert all(isinstance(result, AthleteResult) for result in response) + + # Check successful athletes + assert len(response) == 1 + assert response[0].name == f"Name_{formatted_time}" + assert response[0].successful is True + assert response[0].id == "athlete_id_1" + assert response[0].reason == [] + +# Successful call with env +@patch('hdforce.UpdateAthletes.requests.put') +def test_UpdateAthletes_env(mock_put): + # Get the current time + current_time = datetime.now() + + # Format the current time as a string + formatted_time = current_time.strftime("%Y%m%d%H%M%S") + + # Mock the PUT request response + mock_put.return_value = MagicMock(status_code=200, json=lambda: mock_success_response(formatted_time)) + + # Authenticate + AuthManager() + + # Create New Athletes + players = [ + Athlete(id="athlete_id_1", name=f"Name_{formatted_time}", active=True) + ] + + # Update Athlete + response = UpdateAthletes(athletes=players) + + # Check response is a list of AthleteResult + assert isinstance(response, list) + assert all(isinstance(result, AthleteResult) for result in response) + + # Check successful athletes + assert len(response) == 1 + assert response[0].name == f"Name_{formatted_time}" + assert response[0].successful is True + assert response[0].id == "athlete_id_1" + assert response[0].reason == [] + +# Test for failure response +@patch('hdforce.UpdateAthletes.requests.put') +def test_UpdateAthletes_failure(mock_put): + # Get the current time + current_time = datetime.now() + + # Format the current time as a string + formatted_time = current_time.strftime("%Y%m%d%H%M%S") + + # Mock the PUT request response + mock_put.return_value = MagicMock(status_code=200, json=lambda: mock_failure_response(formatted_time)) + + # Authenticate + AuthManager(authMethod="file", env_file_name="tests/.env") + + # Create New Athletes + players = [ + Athlete(id="athlete_id_1", name=f"Name_{formatted_time}", active=True) + ] + + # Update Athlete + response = UpdateAthletes(athletes=players) + + # Check response is a list of AthleteResult + assert isinstance(response, list) + assert all(isinstance(result, AthleteResult) for result in response) + + # Check failures + assert len(response) == 1 + assert response[0].name == f"Name_{formatted_time}" + assert response[0].successful is False + assert response[0].id == "athlete_id_1" + assert response[0].reason == ['Duplicate or Invalid Athlete Name'] \ No newline at end of file