Repository: FusionAuth/fusionauth-python-client (language: python)

Issue:
Upgrading dependencies, rewriting README and running tests
- [x] Upgrading the library to the latest version of Python and its dependencies
- [x] Rewriting README onto the new standard
- [x] Make sure GitHub Actions are running well

---
- To see the specific tasks where the Asana app for GitHub is being used, see below:
  - https://app.asana.com/0/0/1206547298227817


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 91f2e53..34b2232 100644
--- a/README.md
+++ b/README.md
@@ -1,55 +1,177 @@
-## FusionAuth Python Client ![semver 2.0.0 compliant](http://img.shields.io/badge/semver-2.0.0-brightgreen.svg?style=flat-square)
-If you're integrating FusionAuth with a Python 3 application, this library will speed up your development time.
+# FusionAuth Python Client ![semver 2.0.0 compliant](http://img.shields.io/badge/semver-2.0.0-brightgreen.svg?style=flat-square)
+
+## Intro
+
+<!--
+tag::forDocSite[]
+-->
+
+If you're integrating FusionAuth with a Python 3 application, this library will speed up your development time. Please also make sure to check our [SDK Usage Suggestions page](https://fusionauth.io/docs/sdks/#usage-suggestions).
 
 For additional information and documentation on FusionAuth refer to [https://fusionauth.io](https://fusionauth.io).
 
-### Install
+## Install
 To install the FusionAuth Python Client package run:
 
 ```bash
 pip install fusionauth-client
 ```
 
-This library can be found on PyPI
-* https://pypi.org/project/fusionauth-client/
+This library can be found on PyPI at https://pypi.org/project/fusionauth-client/.
+
+## Examples
+
+### Set Up
+
+First, you have to make sure you have a running FusionAuth instance. If you don't have one already, the easiest way to install FusionAuth is [via Docker](https://fusionauth.io/docs/get-started/download-and-install/docker), but there are [other ways](https://fusionauth.io/docs/get-started/download-and-install). By default, it'll be running on `localhost:9011`.
+
+Then, you have to [create an API Key](https://fusionauth.io/docs/apis/authentication#managing-api-keys) in the admin UI to allow calling API endpoints.
 
-### Coding
-And then include the package in your code by using the following statement.
+You are now ready to use this library.
+
+### Create the Client
+
+Include the package in your code by using the following statement.
 
 ```python
 from fusionauth.fusionauth_client import FusionAuthClient
 ```
 
-Now you're ready to begin making requests to FusionAuth. You will need to supply an API key you created in FusionAuth, the folowing example assumes an API key of `6b87a398-39f2-4692-927b-13188a81a9a3`.
+Now you're ready to begin making requests to FusionAuth. You will need to supply an API key you created in FusionAuth, the following example assumes an API key of `6b87a398-39f2-4692-927b-13188a81a9a3`.
 
 ```python
 client = FusionAuthClient('6b87a398-39f2-4692-927b-13188a81a9a3', 'http://localhost:9011')
 ```
 
-Here's an example which logs a user in:
+### Error Handling
+
+After every request is made, you need to check for any errors and handle them. To avoid cluttering things up, we'll omit the error handling in the next examples, but you should do something like the following.
+
+
+### Create an Application
+
+To create an [Application](https://fusionauth.io/docs/get-started/core-concepts/applications), use the `create_application()` method.
 
 ```python
-from fusionauth.fusionauth_client import FusionAuthClient
-client = FusionAuthClient(API_KEY, 'http://localhost:9011')
+data = {
+    'application': {
+        'name': 'ChangeBank'
+    }
+}
+
+result = client.create_application(data)
+
+# Handle errors as shown in the beginning of the Examples section
+
+# Otherwise parse the successful response
+print(result.success_response)
+```
+
+[Check the API docs for this endpoint](https://fusionauth.io/docs/apis/applications#create-an-application)
 
+### Adding Roles to an Existing Application
+
+To add [roles to an Application](https://fusionauth.io/docs/get-started/core-concepts/applications#roles), use `create_application_role()`.  
+
+```python
 data = {
-    'loginId': loginId,
-    'password': password,
-    'applicationId': My_App_ID
+    'role': {
+        'name': 'customer',
+        'description': 'Default role for regular customers',
+        'isDefault': 1
+    }
 }
 
-print(client.login(data).success_response)
+result = client.create_application_role(
+    application_id='5a89377e-a250-4b15-b766-377ecc9b9fc9',
+    request=data
+)
+
+# Handle errors as shown in the beginning of the Examples section
+
+# Otherwise parse the successful response
+print(result.success_response)
+```
+
+[Check the API docs for this endpoint](https://fusionauth.io/docs/apis/applications#create-an-application-role)
+
+### Retrieve Application Details
+
+To fetch details about an [Application](https://fusionauth.io/docs/get-started/core-concepts/applications), use `retrieve_application()`. 
+
+```python
+result = client.retrieve_application(
+    application_id='5a89377e-a250-4b15-b766-377ecc9b9fc9'
+)
+print(result.success_response)
+```
+
+[Check the API docs for this endpoint](https://fusionauth.io/docs/apis/applications#retrieve-an-application)
+
+### Delete an Application
+
+To delete an [Application](https://fusionauth.io/docs/get-started/core-concepts/applications), use `delete_application()`.
+
+```python
+client.delete_application(
+    application_id='5a89377e-a250-4b15-b766-377ecc9b9fc9'
+)
+```
+
+[Check the API docs for this endpoint](https://fusionauth.io/docs/apis/applications#delete-an-application)
+
+### Lock a User
+
+To [prevent a User from logging in](https://fusionauth.io/docs/get-started/core-concepts/users), use `deactivate_user()`. 
+
+```python
+client.deactivate_user(
+    user_id='231b982c-9304-4642-9bac-492d6917f5aa'
+)
+```
+
+
+[Check the API docs for this endpoint](https://fusionauth.io/docs/apis/users#delete-a-user)
+
+### Registering a User
+
+To [register a User in an Application](https://fusionauth.io/docs/get-started/core-concepts/users#registrations), use `register()`.
+
+The code below also adds a `customer` role and a custom `appBackgroundColor` property to the User Registration.
+
+```python
+result = client.register(
+    user_id='231b982c-9304-4642-9bac-492d6917f5aa',
+    request={
+        'registration': {
+            'applicationId': '5a89377e-a250-4b15-b766-377ecc9b9fc9',
+            'roles': [
+                'customer'
+            ],
+            'data': {
+                'appBackgroundColor': '#096324'
+            }
+        }
+    }
+)
+print(result.success_response)
 ```
 
-Each method in the client library includes documentation to describe the use and parameters. In addition to this resource, review the API documentation. https://fusionauth.io/docs/v1/tech/apis/
+[Check the API docs for this endpoint](https://fusionauth.io/docs/apis/registrations#create-a-user-registration-for-an-existing-user)
+
+<!--
+end::forDocSite[]
+-->
+
 
-If you encounter a bug with this library, please open an issue.
 
 ## Questions and support
 
-If you have a question or support issue regarding this client library, we'd love to hear from you.
+If you find any bugs in this library, [please open an issue](https://github.com/FusionAuth/fusionauth-python-client/issues). Note that changes to the `FusionAuthClient` class have to be done on the [FusionAuth Client Builder repository](https://github.com/FusionAuth/fusionauth-client-builder/blob/master/src/main/client/python.client.ftl), which is responsible for generating that file.
+
+But if you have a question or support issue, we'd love to hear from you.
 
-If you have a paid edition with support included, please [open a ticket in your account portal](https://account.fusionauth.io/account/support/). Learn more about [paid editions here](https://fusionauth.io/pricing).
+If you have a paid plan with support included, please [open a ticket in your account portal](https://account.fusionauth.io/account/support/). Learn more about [paid plan here](https://fusionauth.io/pricing).
 
 Otherwise, please [post your question in the community forum](https://fusionauth.io/community/forum/).
 
@@ -57,9 +179,11 @@ Otherwise, please [post your question in the community forum](https://fusionauth
 
 Bug reports and pull requests are welcome on GitHub at https://github.com/FusionAuth/fusionauth-python-client.
 
+Note: if you want to change the `FusionAuthClient` class, you have to do it on the [FusionAuth Client Builder repository](https://github.com/FusionAuth/fusionauth-client-builder/blob/master/src/main/client/python.client.ftl), which is responsible for generating all client libraries we support.
+
 ## License
 
-This code is available as open source under the terms of the [Apache v2.0 License](https://opensource.org/licenses/Apache-2.0).
+This code is available as open source under the terms of the [Apache v2.0 License](https://opensource.org/license/apache-2-0).
 
 ## Upgrade Policy
 
diff --git a/build.savant b/build.savant
index 2495c9b..72bd64d 100644
--- a/build.savant
+++ b/build.savant
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018-2023, FusionAuth, All Rights Reserved
+ * Copyright (c) 2018-2024, FusionAuth, All Rights Reserved
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -80,7 +80,7 @@ target(name: "setup-python", description: "Gets the python dependencies") {
   proc1.consumeProcessOutput(System.out, System.err)
   proc1.waitFor()
 
-  def proc2 = "python3 -m pip install --user --upgrade wheel twine unittest2 requests deprecated".execute()
+  def proc2 = "python3 -m pip install --user --upgrade wheel twine requests deprecated".execute()
   proc2.consumeProcessOutput(System.out, System.err)
   proc2.waitFor()
 }


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 9157589..022ee22 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -3,9 +3,10 @@ name: run_tests
 
 # Controls when the action will run. 
 on:
-  #push:
-
-  # Allows you to run this workflow manually from the Actions tab
+  push:
+    branches: [ "master" ]
+  pull_request:
+    branches: [ "master" ]
   workflow_dispatch:
 
 # A workflow run is made up of one or more jobs that can run sequentially or in parallel
@@ -21,18 +22,16 @@ jobs:
     # Steps represent a sequence of tasks that will be executed as part of the job
     steps:
       # Setup the system with the repository code, Java, and Ruby
-      - uses: actions/checkout@v2
-      - uses: isbang/compose-action@v1.0.0
+      - uses: actions/checkout@v4
+      - uses: isbang/compose-action@v1.5.1
         with:
           compose-file: './src/test/docker/docker-compose.yml'
           down-flags: '--volumes'
-      - uses: actions/setup-python@v3
+      - uses: actions/setup-python@v5
         with:
-          python-version: '3.9.x'
+          python-version: '3.12.2'
       - name: Install fusionauth library
-        run: |
-          pip3 install .
-          pip3 install unittest2
+        run: pip3 install .
         shell: bash
       - name: Check to see if FusionAuth is loaded
         run: |
diff --git a/src/test/docker/docker-compose.yml b/src/test/docker/docker-compose.yml
index 4aaf7c1..7d20224 100644
--- a/src/test/docker/docker-compose.yml
+++ b/src/test/docker/docker-compose.yml
@@ -2,13 +2,18 @@ version: '3'
 
 services:
   db:
-    image: postgres:12.9
+    image: postgres:16.0-alpine
     environment:
       PGDATA: /var/lib/postgresql/data/pgdata
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
+    healthcheck:
+      test: [ "CMD-SHELL", "pg_isready -U postgres" ]
+      interval: 5s
+      timeout: 5s
+      retries: 5
     networks:
-      - db
+      - db_net
     restart: unless-stopped
     volumes:
       - db_data:/var/lib/postgresql/data
@@ -16,7 +21,8 @@ services:
   fusionauth:
     image: fusionauth/fusionauth-app:latest
     depends_on:
-      - db
+      db:
+        condition: service_healthy
     environment:
       DATABASE_URL: jdbc:postgresql://db:5432/fusionauth
       DATABASE_ROOT_USERNAME: postgres
@@ -25,23 +31,28 @@ services:
       DATABASE_PASSWORD: fusionauth
       FUSIONAUTH_APP_MEMORY: 512M
       FUSIONAUTH_APP_RUNTIME_MODE: development
+      FUSIONAUTH_APP_SILENT_MODE: true
       FUSIONAUTH_APP_URL: http://fusionauth:9011
-      SEARCH_TYPE: database
       FUSIONAUTH_APP_KICKSTART_FILE: /usr/local/fusionauth/kickstart/kickstart.json
-
+      SEARCH_TYPE: database
     networks:
-     - db
+      - db_net
     restart: unless-stopped
     ports:
       - 9011:9011
     volumes:
-      - fa_config:/usr/local/fusionauth/config
+      - fusionauth_config:/usr/local/fusionauth/config
       - ./kickstart:/usr/local/fusionauth/kickstart
+    healthcheck:
+      test: curl --silent --fail http://localhost:9011/api/status -o /dev/null -w "%{http_code}"
+      interval: 5s
+      timeout: 5s
+      retries: 5
 
 networks:
-  db:
+  db_net:
     driver: bridge
 
 volumes:
   db_data:
-  fa_config:
+  fusionauth_config:
diff --git a/src/test/docker/poll-for-kickstart-finish.sh b/src/test/docker/poll-for-kickstart-finish.sh
index 8f092b4..fa2e710 100644
--- a/src/test/docker/poll-for-kickstart-finish.sh
+++ b/src/test/docker/poll-for-kickstart-finish.sh
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2022, FusionAuth, All Rights Reserved
+# Copyright (c) 2024, FusionAuth, All Rights Reserved
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -13,13 +13,26 @@
 # either express or implied. See the License for the specific
 # language governing permissions and limitations under the License.
 #
-imgid=`docker container ls|grep fusiona|awk '{print $1}'`
 
-cnt=`docker logs $imgid 2>&1|grep 'Starting FusionAuth HTTP server' |wc -l`
+cd $(dirname "$0")
 
-while [ $cnt != "1" ]; do
-  echo "waiting $cnt $imgid";
-  cnt=`docker logs $imgid 2>&1|grep 'Starting FusionAuth HTTP server' |wc -l`;
-  sleep 5;
+isFusionAuthReady () {
+  docker compose logs fusionauth 2>&1 | grep -Fq 'Completed [POST] request to [/api/user/registration/00000000-0000-0000-0000-000000000008]'
+}
+
+max_retries=20
+i=1
+while [ "$i" -le "$max_retries" ]; do
+  echo -n "[$i/$max_retries] Waiting for FusionAuth server to start... "
+
+  if isFusionAuthReady; then
+    echo "READY"
+    exit 0
+  fi
+
+  echo "NOT READY"
+  sleep 5
+  i=$((i + 1))
 done
 
+exit 1
\ No newline at end of file
diff --git a/src/test/python/fusionauth/fusionauth_client_test.py b/src/test/python/fusionauth/fusionauth_client_test.py
index 50f1055..dd72caf 100644
--- a/src/test/python/fusionauth/fusionauth_client_test.py
+++ b/src/test/python/fusionauth/fusionauth_client_test.py
@@ -1,4 +1,4 @@
-#  Copyright (c) 2022, FusionAuth, All Rights Reserved
+#  Copyright (c) 2024, FusionAuth, All Rights Reserved
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -28,7 +28,7 @@
 import os
 import uuid
 
-import unittest2
+import unittest
 
 from fusionauth.fusionauth_client import FusionAuthClient
 
@@ -36,10 +36,12 @@
 def print_json(parsed_json):
     print(json.dumps(parsed_json, indent=2, sort_keys=True))
 
-class FusionAuthClientTest(unittest2.TestCase):
+
+class FusionAuthClientTest(unittest.TestCase):
     def setUp(self):
         fusionauth_url = os.getenv('FUSIONAUTH_URL') if 'FUSIONAUTH_URL' in os.environ else 'http://localhost:9011'
-        fusionauth_api_key = os.getenv('FUSIONAUTH_API_KEY') if 'FUSIONAUTH_API_KEY' in os.environ else 'bf69486b-4733-4470-a592-f1bfce7af580'
+        fusionauth_api_key = os.getenv(
+            'FUSIONAUTH_API_KEY') if 'FUSIONAUTH_API_KEY' in os.environ else 'bf69486b-4733-4470-a592-f1bfce7af580'
         self.client = FusionAuthClient(fusionauth_api_key, fusionauth_url)
         self.anonymous_client = FusionAuthClient('', fusionauth_url)
 
@@ -53,7 +55,7 @@ def test_retrieve_applications(self):
 
     def test_create_user_retrieve_user(self):
         # Check if the user already exists.
-        get_user_response = self.client.retrieve_user_by_email('art@vandaleyindustries.com')
+        get_user_response = self.client.retrieve_user_by_email('user@example.com')
         if get_user_response.status == 200:
             delete_user_response = self.client.delete_user(get_user_response.success_response['user']['id'])
             self.assertEqual(delete_user_response.status, 200, delete_user_response.error_response)
@@ -65,7 +67,7 @@ def test_create_user_retrieve_user(self):
             'sendSetPasswordEmail': False,
             'skipVerification': True,
             'user': {
-                'email': 'art@vandaleyindustries.com',
+                'email': 'user@example.com',
                 'password': 'password'
             }
         }
@@ -78,7 +80,7 @@ def test_create_user_retrieve_user(self):
         self.assertEqual(get_user_response.status, 200)
         self.assertIsNotNone(get_user_response.success_response)
         self.assertIsNone(get_user_response.error_response)
-        self.assertEquals(get_user_response.success_response['user']['email'], 'art@vandaleyindustries.com')
+        self.assertEqual(get_user_response.success_response['user']['email'], 'user@example.com')
         self.assertFalse('password' in get_user_response.success_response['user'])
         self.assertFalse('salt' in get_user_response.success_response['user'])
 
@@ -106,4 +108,4 @@ def test_login_user_logout_global(self):
 
 
 if __name__ == '__main__':
-    unittest2.main()
+    unittest.main()
diff --git a/src/test/python/fusionauth/rest_client_test.py b/src/test/python/fusionauth/rest_client_test.py
index 41b31b0..47a8c0c 100644
--- a/src/test/python/fusionauth/rest_client_test.py
+++ b/src/test/python/fusionauth/rest_client_test.py
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2016-2018, FusionAuth, All Rights Reserved
+# Copyright (c) 2016-2024, FusionAuth, All Rights Reserved
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -18,14 +18,16 @@
 import os
 import uuid
 
-import unittest2
+import unittest
 
 from fusionauth.rest_client import RESTClient
 
+
 def print_json(parsed_json):
     print(json.dumps(parsed_json, indent=2, sort_keys=True))
 
-class RestClientTest(unittest2.TestCase):
+
+class RestClientTest(unittest.TestCase):
     def setUp(self):
         pass
 
@@ -54,4 +56,4 @@ def test_uri_with_slash_suffix_with_path_with_no_slash_prefix(self):
 
 
 if __name__ == '__main__':
-    unittest2.main()
+    unittest.main()