├── requirements.txt ├── .gitignore ├── assets └── logo.jpg ├── directus ├── __init__.py └── clients.py ├── tests ├── add_access_token.py ├── test_3_directus_modes.py ├── test_1_directus_v9_static_token.py └── test_2_directus_v9_temp_token.py ├── setup.py ├── locust.py ├── Makefile ├── LICENSE ├── docker-compose.yml ├── .github └── workflows │ └── test.yaml ├── README.md └── coverage.xml /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | uuid 3 | pytest 4 | pytest-cov -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .pytest_cache 3 | *.egg-info 4 | .vscode 5 | htmlcov 6 | .coverage 7 | -------------------------------------------------------------------------------- /assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-CKY/directus-sdk-python/HEAD/assets/logo.jpg -------------------------------------------------------------------------------- /directus/__init__.py: -------------------------------------------------------------------------------- 1 | from directus.clients import DirectusClient_V9 2 | 3 | __version__ = '1.0.0' -------------------------------------------------------------------------------- /tests/add_access_token.py: -------------------------------------------------------------------------------- 1 | from directus.clients import DirectusClient_V9 2 | import os 3 | url = os.environ.get("BASE_URL", "http://localhost:8055") 4 | 5 | client = DirectusClient_V9(url=url, email="admin@example.com", password="password") 6 | 7 | id = client.get("/users/me")['id'] 8 | 9 | client.patch(f'/users/{id}', json={"token": "admin"}) 10 | 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from directus import __version__ 3 | 4 | setup( 5 | name='directus-sdk', 6 | version=__version__, 7 | description='python SDK for directus client wiht convenience functions', 8 | url='https://github.com/Jason-CKY/directus-sdk-python', 9 | author='Jason Cheng', 10 | packages=find_packages(exclude=['examples']), 11 | install_requires=['requests'] 12 | ) -------------------------------------------------------------------------------- /locust.py: -------------------------------------------------------------------------------- 1 | from locust import HttpUser, task, between 2 | 3 | token = 'admin' 4 | class HelloWorldUser(HttpUser): 5 | wait_time = between(1, 10) 6 | 7 | @task 8 | def get_data_test(self): 9 | self.client.get("/items/test_collection", headers={"Authorization": f"Bearer {token}"}) 10 | 11 | @task 12 | def get_data_duplicated(self): 13 | self.client.get("/items/duplicated_collection", headers={"Authorization": f"Bearer {token}"}) 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | # declares .PHONY which will run the make command even if a file of the same name exists 4 | .PHONY: help 5 | help: ## Help command 6 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 7 | 8 | lint: ## Lint check 9 | docker run --rm -v $(PWD):/src:Z \ 10 | --workdir=/src odinuge/yapf:latest yapf \ 11 | --style '{based_on_style: pep8, dedent_closing_brackets: true, coalesce_brackets: true}' \ 12 | --no-local-style --verbose --recursive --diff --parallel directus 13 | 14 | format: ## Format code in place to conform to lint check 15 | docker run --rm -v $(PWD):/src:Z \ 16 | --workdir=/src odinuge/yapf:latest yapf \ 17 | --style '{based_on_style: pep8, dedent_closing_brackets: true, coalesce_brackets: true}' \ 18 | --no-local-style --verbose --recursive --in-place --parallel directus 19 | 20 | pyflakes: ## Pyflakes check for any unused variables/classes 21 | docker run --rm -v $(PWD):/src:Z \ 22 | --workdir=/src python:3.8 \ 23 | /bin/bash -c "pip install --upgrade pyflakes && python -m pyflakes /src && echo 'pyflakes passed!'" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Cheng Kuan Yong Jason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | database: 4 | container_name: database 5 | image: postgis/postgis:13-master 6 | networks: 7 | - directus 8 | environment: 9 | POSTGRES_USER: 'directus' 10 | POSTGRES_PASSWORD: 'directus' 11 | POSTGRES_DB: 'directus' 12 | 13 | directus: 14 | container_name: directus 15 | image: directus/directus:latest 16 | restart: on-failure 17 | ports: 18 | - 8055:8055 19 | networks: 20 | - directus 21 | depends_on: 22 | - database 23 | environment: 24 | KEY: '255d861b-5ea1-5996-9aa3-922530ec40b1' 25 | SECRET: '6116487b-cda1-52c2-b5b5-c8022c45e263' 26 | 27 | DB_CLIENT: 'pg' 28 | DB_HOST: 'database' 29 | DB_PORT: '5432' 30 | DB_DATABASE: 'directus' 31 | DB_USER: 'directus' 32 | DB_PASSWORD: 'directus' 33 | 34 | ADMIN_EMAIL: 'admin@example.com' 35 | ADMIN_PASSWORD: 'password' 36 | 37 | # Make sure to set this in production 38 | # (see https://docs.directus.io/configuration/config-options/#general) 39 | # PUBLIC_URL: 'https://directus.example.com' 40 | 41 | networks: 42 | directus: 43 | -------------------------------------------------------------------------------- /tests/test_3_directus_modes.py: -------------------------------------------------------------------------------- 1 | # import sys 2 | # sys.path.append('./directus') 3 | from directus.clients import DirectusClient_V9 4 | import pytest 5 | import os 6 | 7 | url = os.environ.get("BASE_URL", "http://localhost:8055") 8 | 9 | client = DirectusClient_V9(url=url, email="admin@example.com", password="password") 10 | 11 | @pytest.mark.parametrize( 12 | "collection_name, pk_type", [ 13 | ("test_collection", "int") 14 | ] 15 | ) 16 | def test_create_collection(collection_name: str, pk_type: str): 17 | collection_data = {"collection": collection_name, "schema": {}, "meta": {}} 18 | if pk_type == 'uuid': 19 | collection_data['fields'] = [{ 20 | "field": "id", 21 | "type": "uuid", 22 | "meta": { 23 | "hidden": True, 24 | "readonly": True, 25 | "interface": "text-input", 26 | "special": ["uuid"] 27 | }, 28 | "schema": { 29 | "is_primary_key": True 30 | } 31 | }] 32 | 33 | client.post("/collections", json=collection_data) 34 | 35 | @pytest.mark.parametrize( 36 | "collection_name, field_name, field_type", 37 | [("test_collection", "name", "string")] 38 | ) 39 | def test_login_modes(collection_name: str, field_name: str, field_type: str): 40 | client.post( 41 | f"/fields/{collection_name}", 42 | json={ 43 | "field": field_name, 44 | "type": field_type 45 | } 46 | ) 47 | client.logout() 48 | client.static_token = "admin" 49 | client.delete(f"/fields/{collection_name}/{field_name}") 50 | client.static_token = None 51 | client.login(email="admin@example.com", password="password") 52 | client.post( 53 | f"/fields/{collection_name}", 54 | json={ 55 | "field": field_name, 56 | "type": field_type 57 | } 58 | ) 59 | 60 | @pytest.mark.parametrize( 61 | "collection_name", 62 | [ 63 | ("test_collection") 64 | ] 65 | ) 66 | def test_delete_collection(collection_name: str): 67 | client.delete(f"/collections/{collection_name}") 68 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'badges' 6 | 7 | jobs: 8 | build: 9 | name: Run Python Tests 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: isbang/compose-action@v1.0.0 15 | with: 16 | compose-file: './docker-compose.yml' 17 | down-flags: '--volumes' 18 | 19 | - name: Set up Python 3.6 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.6 23 | 24 | - name: Install Python dependencies 25 | run: | 26 | sudo apt install -y $(grep -o ^[^#][[:alnum:]-]* "packages.list") 27 | python3 -m pip install --upgrade pip 28 | pip3 install -r requirements.txt 29 | pip3 install -e . 30 | 31 | - name: Test with pytest 32 | id: pytest 33 | run: | 34 | python3 tests/add_access_token.py 35 | pytest --exitfirst --verbose --failed-first --cov=. --cov-report html 36 | total=$(cat htmlcov/index.html | grep -i -C 4 total | tail -n 1 | grep -Eo '[0-9]+%|[0-9]+\.[0-9]+%' | grep -Eo '[0-9]+|[0-9]+\.[0-9]+') 37 | if (( $(echo "$total <= 50" | bc -l) )) ; then 38 | COLOR=red 39 | elif (( $(echo "$total > 80" | bc -l) )); then 40 | COLOR=green 41 | else 42 | COLOR=orange 43 | fi 44 | 45 | # Generates a GitHub Workflow output named `lines` with a coverage value 46 | echo "##[set-output name=coverage;]${total}" 47 | echo "##[set-output name=color;]${COLOR}" 48 | 49 | # Get current banch name to use it as dest directory 50 | - name: Extract branch name 51 | shell: bash 52 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 53 | id: extract_branch 54 | 55 | - name: Prepare environment 56 | id: coverage 57 | run: | 58 | # Output values to be used by other steps 59 | echo "##[set-output name=path;]${BADGE_PATH}" 60 | echo "##[set-output name=branch;]${BRANCH}" 61 | env: 62 | BADGE_PATH: ${{ steps.extract_branch.outputs.branch }}/coverage-badge.svg 63 | BRANCH: badges 64 | 65 | - uses: actions/checkout@v3 66 | with: 67 | ref: ${{ steps.coverage.outputs.branch }} 68 | 69 | # Create the directory where badges will be saved, if needed 70 | - name: Create destination directory 71 | env: 72 | BADGE_PATH: ${{ steps.coverage.outputs.path }} 73 | run: mkdir -p "${BADGE_PATH%/*}" 74 | 75 | # Use the output from the `coverage` step 76 | - name: Generate the badge SVG image 77 | id: badge 78 | env: 79 | COVERAGE: ${{ steps.pytest.outputs.coverage }} 80 | COLOR: ${{ steps.pytest.outputs.color }} 81 | BADGE_PATH: ${{ steps.coverage.outputs.path }} 82 | run: curl "https://img.shields.io/badge/coverage-$COVERAGE%25-$COLOR" > ${BADGE_PATH} 83 | 84 | - name: Upload badge as artifact 85 | uses: actions/upload-artifact@v3 86 | with: 87 | name: badge 88 | path: ${{ steps.coverage.outputs.path }} 89 | if-no-files-found: error 90 | 91 | - name: Commit badge 92 | continue-on-error: true 93 | env: 94 | BADGE: ${{ steps.coverage.outputs.path }} 95 | run: | 96 | git config --local user.email "action@github.com" 97 | git config --local user.name "GitHub Action" 98 | git add "${BADGE}" 99 | git commit -m "Add/Update badge" 100 | - name: Push badge commit 101 | uses: ad-m/github-push-action@master 102 | if: ${{ success() }} 103 | with: 104 | github_token: ${{ secrets.GITHUB_TOKEN }} 105 | branch: ${{ steps.coverage.outputs.branch }} 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Directus Logo Directus Python SDK 3 |

4 | 5 | [![Tests](https://github.com/Jason-CKY/directus-sdk-python/actions/workflows/test.yaml/badge.svg)](https://github.com/Jason-CKY/directus-sdk-python/actions/workflows/test.yaml) ![coverage](https://raw.githubusercontent.com/Jason-CKY/directus-sdk-python/badges/main/coverage-badge.svg) 6 | 7 | ## Requirements 8 | 9 | - Python 3.6+ 10 | 11 | ## Installation 12 | 13 | ```bash 14 | pip install -e . 15 | ``` 16 | 17 | ## Usage 18 | 19 | ### Initializa directus client 20 | 21 | ```python 22 | from directus.clients import DirectusClient_V9 23 | 24 | # Create a directus client connection with user static token 25 | client = DirectusClient_V9(url="http://localhost:8055", token="admin-token") 26 | 27 | # Or create a directus client connection with email and password 28 | client = DirectusClient_V9(url="http://localhost:8055", email="user@example.com", password="password") 29 | ``` 30 | 31 | ### Logging in and out of the client 32 | 33 | ```python 34 | client = DirectusClient_V9(url="http://localhost:8055", email="user@example.com", password="password") 35 | 36 | # Log out and use static token instead 37 | client.logout() 38 | client.static_token = "admin-token" 39 | client.login(email="user2@example.com", password="password2") 40 | ``` 41 | 42 | ### Generic API requests 43 | 44 | The directus client automatically handles the injection of access token so any [directus API requests](https://docs.directus.io/reference/introduction/) can be simplified like so: 45 | 46 | ```python 47 | # GET request 48 | collection = client.get(f"/collections/{collection_name}") 49 | item = client.get(f"/items/{collection_name}/1") 50 | 51 | # POST request 52 | items = [ 53 | { 54 | "name": "item1" 55 | }, 56 | { 57 | "name": "item2" 58 | } 59 | ] 60 | 61 | client.post(f"/items/{collection_name}", json=items) 62 | 63 | # PATCH request 64 | client.patch(f"/items/{collection_name}/1", json={ 65 | "name": "updated item1" 66 | }) 67 | 68 | # DELETE request 69 | client.delete(f"/items/{collection_name}/1") 70 | ``` 71 | 72 | #### Bulk Insert 73 | 74 | > **Params:** collection_name: str, items: list 75 | 76 | ```python 77 | client.bulk_insert(collection_name="test-collection", 78 | items=[{"Title": "test"}, {"Title": "test2"}]) 79 | ``` 80 | 81 | #### Duplicate Collection 82 | 83 | > **Params:** collection_name: str, duplicate_collection_name: str 84 | 85 | ```python 86 | client.duplicate_collection(collection_name="test-collection", duplicate_collection_name="test_duplication_collection") 87 | ``` 88 | 89 | #### Checks if collection exists 90 | 91 | > **Params** collection_name: str, items: list 92 | 93 | ```python 94 | if client.collection_exists("test"): 95 | print("test collection exists!") 96 | ``` 97 | 98 | #### Delete all items from a collection 99 | 100 | > **Params:** collection_name: str 101 | 102 | ```python 103 | client.delete_all_items("test") 104 | ``` 105 | 106 | #### Get collection primary key 107 | 108 | > **Params:** collection_name: str 109 | 110 | ```python 111 | pk_field = client.get_pk_field("brands") 112 | ``` 113 | 114 | #### Get all user-created collection names 115 | 116 | > **Params:** 117 | 118 | ```python 119 | print("Listing all user-created collections on directus...") 120 | for name in client.get_all_user_created_collection_names(): 121 | print(name) 122 | ``` 123 | 124 | #### Get all field names of a given collection 125 | 126 | > **Params:** collection_name: str 127 | 128 | ```python 129 | print("Listing all fields in test collection...") 130 | for field in client.get_all_fields("test"): 131 | print(json.dumps(field, indent=4)) 132 | ``` 133 | 134 | #### Get all foreign key fields in directus collection 135 | 136 | > **Params:** collection_name: str 137 | 138 | ```python 139 | print("Listing all foreign key fields in test collection...") 140 | for field in client.get_all_fk_fields("brands"): 141 | print(json.dumps(field, indent=4)) 142 | ``` 143 | 144 | #### Get all relations in directus collection 145 | 146 | > **Params:** collection_name: str 147 | 148 | ```python 149 | import json 150 | print("Listing all relations in test collection...") 151 | for field in client.get_relations("test"): 152 | print(json.dumps(field, indent=4)) 153 | ``` 154 | 155 | #### Create relations 156 | 157 | > **Params:** relation: dict 158 | 159 | ```python 160 | client.post_relation({ 161 | "collection": "books", 162 | "field": "author", 163 | "related_collection": "authors" 164 | }) 165 | ``` 166 | 167 | 168 | ## TODOs: 169 | 170 | * debug test cases 171 | * add proper pytest -------------------------------------------------------------------------------- /tests/test_1_directus_v9_static_token.py: -------------------------------------------------------------------------------- 1 | # import sys 2 | # sys.path.append('./directus') 3 | from directus.clients import DirectusClient_V9 4 | import pytest, uuid 5 | import os 6 | 7 | url = os.environ.get("BASE_URL", "http://localhost:8055") 8 | 9 | client = DirectusClient_V9(url=url, token="admin") 10 | 11 | @pytest.mark.parametrize( 12 | "collection_name, pk_type", [ 13 | ("test_collection", "int"), 14 | ("test_relations_collection", "int"), 15 | ("uuid_pk_collection", "uuid") 16 | ] 17 | ) 18 | def test_create_collection(collection_name: str, pk_type: str): 19 | collection_data = {"collection": collection_name, "schema": {}, "meta": {}} 20 | if pk_type == 'uuid': 21 | collection_data['fields'] = [{ 22 | "field": "id", 23 | "type": "uuid", 24 | "meta": { 25 | "hidden": True, 26 | "readonly": True, 27 | "interface": "text-input", 28 | "special": ["uuid"] 29 | }, 30 | "schema": { 31 | "is_primary_key": True 32 | } 33 | }] 34 | 35 | client.post("/collections", json=collection_data) 36 | 37 | @pytest.mark.parametrize( 38 | "collection_name, field_name, field_type", 39 | [ 40 | ("test_collection", "name", "string"), 41 | ("test_collection", "comments", "text"), 42 | ("test_relations_collection", "uuid_fk", "uuid"), 43 | ("uuid_pk_collection", "title", "string") 44 | ] 45 | ) 46 | def test_create_field(collection_name: str, field_name: str, field_type: str): 47 | client.post( 48 | f"/fields/{collection_name}", 49 | json={ 50 | "field": field_name, 51 | "type": field_type 52 | } 53 | ) 54 | 55 | @pytest.mark.parametrize( 56 | "collection_name, name, comments", 57 | [ 58 | ("test_collection", "item1", "delete me!"), 59 | ("test_collection", "item2", "change me!") 60 | ] 61 | ) 62 | def test_create_items(collection_name: str, name: str, comments: str): 63 | client.post( 64 | f"/items/{collection_name}", 65 | json={ 66 | "name": name, 67 | "comments": comments 68 | } 69 | ) 70 | 71 | @pytest.mark.parametrize( 72 | "collection_name, id", 73 | [ 74 | ("test_collection", 1) 75 | ] 76 | ) 77 | def test_delete_item(collection_name: str, id: int): 78 | client.delete( 79 | f"/items/{collection_name}/{id}" 80 | ) 81 | 82 | @pytest.mark.parametrize( 83 | "collection_name, id, name, comments", 84 | [ 85 | ("test_collection", 2, "item2_updated", "I am updated!") 86 | ] 87 | ) 88 | def test_update_items(collection_name: str, id: int, name: str, comments: str): 89 | client.patch( 90 | f"/items/{collection_name}/{id}", 91 | json={ 92 | "name": name, 93 | "comments": comments 94 | } 95 | ) 96 | 97 | @pytest.mark.parametrize( 98 | "collection_name, num_of_items", 99 | [ 100 | ("test_collection", 100) 101 | ] 102 | ) 103 | def test_bulk_insert(collection_name: str, num_of_items: int): 104 | items = [{ 105 | "name": str(i) + str(uuid.uuid4()), 106 | "comments": str(uuid.uuid4()) 107 | } for i in range(num_of_items)] 108 | client.bulk_insert(collection_name, items) 109 | 110 | @pytest.mark.parametrize( 111 | "original_collection_name, duplicated_collection_name", 112 | [ 113 | ("test_collection", "duplicated_collection") 114 | ] 115 | ) 116 | def test_duplicate_collection(original_collection_name: str, duplicated_collection_name: str): 117 | client.duplicate_collection(original_collection_name, duplicated_collection_name) 118 | 119 | @pytest.mark.parametrize( 120 | "collection_name, exists", 121 | [ 122 | ("test_collection", True), 123 | ("duplicated_collection", True), 124 | ("no_collection", False) 125 | ] 126 | ) 127 | def test_collection_exists(collection_name: str, exists: bool): 128 | assert client.collection_exists(collection_name) == exists 129 | 130 | @pytest.mark.parametrize( 131 | "collection_name", 132 | [ 133 | ("duplicated_collection") 134 | ] 135 | ) 136 | def test_delete_all_items(collection_name: str): 137 | client.delete_all_items(collection_name) 138 | 139 | @pytest.mark.parametrize( 140 | "collection_name, data_type", 141 | [ 142 | ("test_collection", "integer"), 143 | ("duplicated_collection", "integer"), 144 | ("uuid_pk_collection", "uuid") 145 | ] 146 | ) 147 | def test_integer_pk(collection_name: str, data_type: str): 148 | client.get_pk_field(collection_name)['type'] == data_type 149 | 150 | @pytest.mark.parametrize( 151 | "collection_names", 152 | [ 153 | (["test_collection", "duplicated_collection", "uuid_pk_collection", "test_relations_collection"]) 154 | ] 155 | ) 156 | def test_get_all_user_created_collection_names(collection_names: list): 157 | assert sorted(client.get_all_user_created_collection_names()) == sorted(collection_names) 158 | 159 | @pytest.mark.parametrize( 160 | "collection_name, field_names", 161 | [ 162 | ("test_collection", ["id", "name", "comments"]), 163 | ("duplicated_collection", ["id", "name", "comments"]), 164 | ("test_relations_collection", ["id", "uuid_fk"]), 165 | ("uuid_pk_collection", ["id", "title"]), 166 | ] 167 | ) 168 | def test_get_all_fields(collection_name: str, field_names: list): 169 | user_fields = [ 170 | field['field'] for field in client.get_all_fields(collection_name) 171 | ] 172 | 173 | assert sorted(field_names) == sorted(user_fields) 174 | 175 | @pytest.mark.parametrize( 176 | "collection_name, field, related_collection", 177 | [ 178 | ("test_relations_collection", "uuid_fk", "uuid_pk_collection") 179 | ] 180 | ) 181 | def test_post_relations(collection_name: str, field: str, related_collection: str): 182 | client.post_relation({ 183 | "collection": collection_name, 184 | "field": field, 185 | "related_collection": related_collection 186 | }) 187 | 188 | @pytest.mark.parametrize( 189 | "collection_name, fk_field_names", 190 | [ 191 | ("test_relations_collection", ["uuid_fk"]), 192 | ("test_collection", []), 193 | ("duplicated_collection", []), 194 | ("uuid_pk_collection", []) 195 | ] 196 | ) 197 | def test_get_fk(collection_name: str, fk_field_names: list): 198 | fk_fields = [ 199 | field['field'] for field in [ 200 | field for field in client.get_all_fk_fields(collection_name) 201 | if field['schema']['foreign_key_table'] in 202 | client.get_all_user_created_collection_names() 203 | ] 204 | ] 205 | assert sorted(fk_fields) == sorted(fk_field_names) 206 | 207 | @pytest.mark.parametrize( 208 | "collection_name, relations", 209 | [ 210 | ("test_relations_collection", [{ 211 | "collection": "test_relations_collection", 212 | "field": "uuid_fk", 213 | "related_collection": "uuid_pk_collection" 214 | }]), 215 | ("test_collection", []), 216 | ("duplicated_collection", []), 217 | ("uuid_pk_collection", []) 218 | ] 219 | ) 220 | def test_get_relations(collection_name: str, relations: list): 221 | assert client.get_relations(collection_name) == relations 222 | 223 | @pytest.mark.parametrize( 224 | "collection_name", 225 | [ 226 | ("test_collection"), 227 | ("duplicated_collection"), 228 | ("test_relations_collection"), 229 | ("uuid_pk_collection") 230 | ] 231 | ) 232 | def test_delete_collection(collection_name: str): 233 | client.delete(f"/collections/{collection_name}") 234 | 235 | -------------------------------------------------------------------------------- /tests/test_2_directus_v9_temp_token.py: -------------------------------------------------------------------------------- 1 | # import sys 2 | # sys.path.append('./directus') 3 | from directus.clients import DirectusClient_V9 4 | import pytest, uuid 5 | import os 6 | 7 | url = os.environ.get("BASE_URL", "http://localhost:8055") 8 | 9 | client = DirectusClient_V9(url=url, email="admin@example.com", password="password") 10 | 11 | @pytest.mark.parametrize( 12 | "collection_name, pk_type", [ 13 | ("test_collection", "int"), 14 | ("test_relations_collection", "int"), 15 | ("uuid_pk_collection", "uuid") 16 | ] 17 | ) 18 | def test_create_collection(collection_name: str, pk_type: str): 19 | collection_data = {"collection": collection_name, "schema": {}, "meta": {}} 20 | if pk_type == 'uuid': 21 | collection_data['fields'] = [{ 22 | "field": "id", 23 | "type": "uuid", 24 | "meta": { 25 | "hidden": True, 26 | "readonly": True, 27 | "interface": "text-input", 28 | "special": ["uuid"] 29 | }, 30 | "schema": { 31 | "is_primary_key": True 32 | } 33 | }] 34 | 35 | client.post("/collections", json=collection_data) 36 | 37 | @pytest.mark.parametrize( 38 | "collection_name, field_name, field_type", 39 | [ 40 | ("test_collection", "name", "string"), 41 | ("test_collection", "comments", "text"), 42 | ("test_relations_collection", "uuid_fk", "uuid"), 43 | ("uuid_pk_collection", "title", "string") 44 | ] 45 | ) 46 | def test_create_field(collection_name: str, field_name: str, field_type: str): 47 | client.post( 48 | f"/fields/{collection_name}", 49 | json={ 50 | "field": field_name, 51 | "type": field_type 52 | } 53 | ) 54 | 55 | @pytest.mark.parametrize( 56 | "collection_name, name, comments", 57 | [ 58 | ("test_collection", "item1", "delete me!"), 59 | ("test_collection", "item2", "change me!") 60 | ] 61 | ) 62 | def test_create_items(collection_name: str, name: str, comments: str): 63 | client.post( 64 | f"/items/{collection_name}", 65 | json={ 66 | "name": name, 67 | "comments": comments 68 | } 69 | ) 70 | 71 | @pytest.mark.parametrize( 72 | "collection_name, id", 73 | [ 74 | ("test_collection", 1) 75 | ] 76 | ) 77 | def test_delete_item(collection_name: str, id: int): 78 | client.delete( 79 | f"/items/{collection_name}/{id}" 80 | ) 81 | 82 | @pytest.mark.parametrize( 83 | "collection_name, id, name, comments", 84 | [ 85 | ("test_collection", 2, "item2_updated", "I am updated!") 86 | ] 87 | ) 88 | def test_update_items(collection_name: str, id: int, name: str, comments: str): 89 | client.patch( 90 | f"/items/{collection_name}/{id}", 91 | json={ 92 | "name": name, 93 | "comments": comments 94 | } 95 | ) 96 | 97 | @pytest.mark.parametrize( 98 | "collection_name, num_of_items", 99 | [ 100 | ("test_collection", 100) 101 | ] 102 | ) 103 | def test_bulk_insert(collection_name: str, num_of_items: int): 104 | items = [{ 105 | "name": str(i) + str(uuid.uuid4()), 106 | "comments": str(uuid.uuid4()) 107 | } for i in range(num_of_items)] 108 | client.bulk_insert(collection_name, items) 109 | 110 | @pytest.mark.parametrize( 111 | "original_collection_name, duplicated_collection_name", 112 | [ 113 | ("test_collection", "duplicated_collection") 114 | ] 115 | ) 116 | def test_duplicate_collection(original_collection_name: str, duplicated_collection_name: str): 117 | client.duplicate_collection(original_collection_name, duplicated_collection_name) 118 | 119 | @pytest.mark.parametrize( 120 | "collection_name, exists", 121 | [ 122 | ("test_collection", True), 123 | ("duplicated_collection", True), 124 | ("no_collection", False) 125 | ] 126 | ) 127 | def test_collection_exists(collection_name: str, exists: bool): 128 | assert client.collection_exists(collection_name) == exists 129 | 130 | @pytest.mark.parametrize( 131 | "collection_name", 132 | [ 133 | ("duplicated_collection") 134 | ] 135 | ) 136 | def test_delete_all_items(collection_name: str): 137 | client.delete_all_items(collection_name) 138 | 139 | @pytest.mark.parametrize( 140 | "collection_name, data_type", 141 | [ 142 | ("test_collection", "integer"), 143 | ("duplicated_collection", "integer"), 144 | ("uuid_pk_collection", "uuid") 145 | ] 146 | ) 147 | def test_integer_pk(collection_name: str, data_type: str): 148 | client.get_pk_field(collection_name)['type'] == data_type 149 | 150 | @pytest.mark.parametrize( 151 | "collection_names", 152 | [ 153 | (["test_collection", "duplicated_collection", "uuid_pk_collection", "test_relations_collection"]) 154 | ] 155 | ) 156 | def test_get_all_user_created_collection_names(collection_names: list): 157 | assert sorted(client.get_all_user_created_collection_names()) == sorted(collection_names) 158 | 159 | @pytest.mark.parametrize( 160 | "collection_name, field_names", 161 | [ 162 | ("test_collection", ["id", "name", "comments"]), 163 | ("duplicated_collection", ["id", "name", "comments"]), 164 | ("test_relations_collection", ["id", "uuid_fk"]), 165 | ("uuid_pk_collection", ["id", "title"]), 166 | ] 167 | ) 168 | def test_get_all_fields(collection_name: str, field_names: list): 169 | user_fields = [ 170 | field['field'] for field in client.get_all_fields(collection_name) 171 | ] 172 | 173 | assert sorted(field_names) == sorted(user_fields) 174 | 175 | @pytest.mark.parametrize( 176 | "collection_name, field, related_collection", 177 | [ 178 | ("test_relations_collection", "uuid_fk", "uuid_pk_collection") 179 | ] 180 | ) 181 | def test_post_relations(collection_name: str, field: str, related_collection: str): 182 | client.post_relation({ 183 | "collection": collection_name, 184 | "field": field, 185 | "related_collection": related_collection 186 | }) 187 | 188 | @pytest.mark.parametrize( 189 | "collection_name, fk_field_names", 190 | [ 191 | ("test_relations_collection", ["uuid_fk"]), 192 | ("test_collection", []), 193 | ("duplicated_collection", []), 194 | ("uuid_pk_collection", []) 195 | ] 196 | ) 197 | def test_get_fk(collection_name: str, fk_field_names: list): 198 | fk_fields = [ 199 | field['field'] for field in [ 200 | field for field in client.get_all_fk_fields(collection_name) 201 | if field['schema']['foreign_key_table'] in 202 | client.get_all_user_created_collection_names() 203 | ] 204 | ] 205 | assert sorted(fk_fields) == sorted(fk_field_names) 206 | 207 | @pytest.mark.parametrize( 208 | "collection_name, relations", 209 | [ 210 | ("test_relations_collection", [{ 211 | "collection": "test_relations_collection", 212 | "field": "uuid_fk", 213 | "related_collection": "uuid_pk_collection" 214 | }]), 215 | ("test_collection", []), 216 | ("duplicated_collection", []), 217 | ("uuid_pk_collection", []) 218 | ] 219 | ) 220 | def test_get_relations(collection_name: str, relations: list): 221 | assert client.get_relations(collection_name) == relations 222 | 223 | @pytest.mark.parametrize( 224 | "collection_name", 225 | [ 226 | ("test_collection"), 227 | ("duplicated_collection"), 228 | ("test_relations_collection"), 229 | ("uuid_pk_collection") 230 | ] 231 | ) 232 | def test_delete_collection(collection_name: str): 233 | client.delete(f"/collections/{collection_name}") 234 | 235 | -------------------------------------------------------------------------------- /directus/clients.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from urllib3.exceptions import InsecureRequestWarning 3 | 4 | class DirectusClient_V9(): 5 | def __init__(self, url: str, token: str = None, email: str = None, password: str = None, verify: bool = False): 6 | self.verify = verify 7 | if not self.verify: 8 | requests.packages.urllib3.disable_warnings( 9 | category=InsecureRequestWarning 10 | ) 11 | 12 | self.url = url 13 | if token is not None: 14 | self.static_token = token 15 | self.temporary_token = None 16 | elif email is not None and password is not None: 17 | self.email = email 18 | self.password=password 19 | self.login(email, password) 20 | self.static_token = None 21 | else: 22 | self.static_token = None 23 | self.temporary_token = None 24 | 25 | def login(self, email: str = None, password: str = None) -> tuple: 26 | ''' 27 | Login with the /auth/login endpoint. Returns both the access token and refresh token. Updates self.email and self.password 28 | if provided email and passwords 29 | ''' 30 | if email is None or password is None: 31 | email = self.email 32 | password = self.password 33 | else: 34 | self.email = email 35 | self.password = password 36 | 37 | auth = requests.post( 38 | f"{self.url}/auth/login", 39 | json={ 40 | "email": email, 41 | "password": password 42 | } 43 | ).json()['data'] 44 | 45 | self.static_token = None 46 | self.temporary_token = auth['access_token'] 47 | self.refresh_token = auth['refresh_token'] 48 | 49 | def logout(self, refresh_token: str = None) -> None: 50 | ''' 51 | Retrieve new temporary access token and refresh token 52 | ''' 53 | if refresh_token is None: 54 | refresh_token = self.refresh_token 55 | auth = requests.post( 56 | f"{self.url}/auth/logout", 57 | json={"refresh_token": refresh_token}, 58 | verify=self.verify 59 | ) 60 | self.temporary_token = None 61 | self.refresh_token = None 62 | 63 | def refresh(self, refresh_token: str = None) -> None: 64 | ''' 65 | Retrieve new temporary access token and refresh token 66 | ''' 67 | if refresh_token is None: 68 | refresh_token = self.refresh_token 69 | auth = requests.post( 70 | f"{self.url}/auth/refresh", 71 | json={ 72 | "refresh_token": refresh_token 73 | }, 74 | verify=self.verify 75 | ).json()['data'] 76 | 77 | self.temporary_token = auth['access_token'] 78 | self.refresh_token = auth['refresh_token'] 79 | 80 | def get_token(self): 81 | ''' 82 | Returns static token if there is any, if not refreshes the temp token. 83 | ''' 84 | if self.static_token is not None: 85 | token = self.static_token 86 | elif self.temporary_token is not None: 87 | self.refresh() 88 | token = self.temporary_token 89 | else: 90 | token = "" 91 | return token 92 | 93 | def get(self, path, output_type: str = "json", **kwargs): 94 | data = requests.get( 95 | f"{self.url}{path}", 96 | headers={"Authorization": f"Bearer {self.get_token()}"}, 97 | verify=self.verify, 98 | **kwargs 99 | ) 100 | if 'errors' in data.text: 101 | raise AssertionError(data.json()['errors']) 102 | if output_type == 'csv': 103 | return data.text 104 | 105 | return data.json()['data'] 106 | 107 | def post(self, path, **kwargs): 108 | x = requests.post( 109 | f"{self.url}{path}", 110 | headers={"Authorization": f"Bearer {self.get_token()}"}, 111 | verify=self.verify, 112 | **kwargs 113 | ) 114 | if x.status_code != 200: 115 | raise AssertionError(x.text) 116 | 117 | return x.json() 118 | 119 | def delete(self, path, **kwargs): 120 | x = requests.delete( 121 | f"{self.url}{path}", 122 | headers={"Authorization": f"Bearer {self.get_token()}"}, 123 | verify=self.verify, 124 | **kwargs 125 | ) 126 | if x.status_code != 204: 127 | raise AssertionError(x.text) 128 | 129 | def patch(self, path, **kwargs): 130 | x = requests.patch( 131 | f"{self.url}{path}", 132 | headers={"Authorization": f"Bearer {self.get_token()}"}, 133 | verify=self.verify, 134 | **kwargs 135 | ) 136 | 137 | if x.status_code not in [200, 204]: 138 | raise AssertionError(x.text) 139 | 140 | return x.json() 141 | 142 | def bulk_insert(self, collection_name: str, items: list, interval: int = 100, verbose: bool = False) -> None: 143 | ''' 144 | Post items is capped at 100 items. This function breaks up any list of items more than 100 long and bulk insert 145 | ''' 146 | length = len(items) 147 | for i in range(0, length, interval): 148 | if verbose: 149 | print(f"Inserting {i}-{min(i+100, length)} out of {length}") 150 | self.post(f"/items/{collection_name}", json=items[i:i + interval]) 151 | 152 | def duplicate_collection(self, collection_name: str, duplicate_collection_name: str) -> None: 153 | ''' 154 | Duplicate the collection with schema, fields, and data 155 | ''' 156 | duplicate_collection = self.get(f"/collections/{collection_name}") 157 | duplicate_collection['collection'] = duplicate_collection_name 158 | duplicate_collection['meta']['collection'] = duplicate_collection_name 159 | duplicate_collection['schema']['name'] = duplicate_collection_name 160 | self.post("/collections", json=duplicate_collection) 161 | fields = [ 162 | field for field in self.get_all_fields(collection_name) if not field['schema']['is_primary_key'] 163 | ] 164 | for field in fields: 165 | self.post(f"/fields/{duplicate_collection_name}", json=field) 166 | self.bulk_insert(duplicate_collection_name, self.get(f"/items/{collection_name}", params={"limit": -1})) 167 | 168 | def collection_exists(self, collection_name: str): 169 | ''' 170 | Checks if collection exists in directus 171 | ''' 172 | collection_schema = [col['collection'] for col in self.get('/collections')] 173 | return collection_name in collection_schema 174 | 175 | def delete_all_items(self, collection_name: str) -> None: 176 | ''' 177 | Delete all items from the directus collection. Delete api from directus only able to delete based on ID. 178 | This helper function helps to delete every item within the collection. 179 | ''' 180 | pk_name = self.get_pk_field(collection_name)['field'] 181 | item_ids = [data['id'] for data in self.get(f"/items/{collection_name}?fields={pk_name}", params={"limit": -1})] 182 | if len(item_ids) == 0: 183 | raise AssertionError("No items to delete!") 184 | for i in range(0, len(item_ids), 100): 185 | self.delete(f"/items/{collection_name}", json=item_ids[i:i + 100]) 186 | 187 | def get_all_fields(self, collection_name: str) -> list: 188 | ''' 189 | Return all fields in the directus collection. Remove the id key in metya to avoid errors in inseting this directus field again 190 | ''' 191 | fields = self.get(f"/fields/{collection_name}") 192 | for field in fields: 193 | if 'meta' in field and field['meta'] is not None and 'id' in field['meta']: 194 | field['meta'].pop('id') 195 | 196 | return fields 197 | 198 | def get_pk_field(self, collection_name: str) -> dict: 199 | ''' 200 | Return the primary key field of the collection 201 | ''' 202 | return [field for field in self.get(f"/fields/{collection_name}") if field['schema']['is_primary_key']][0] 203 | 204 | def get_all_user_created_collection_names(self) -> list: 205 | ''' 206 | Returns all user created collections. By default Directus GET /collections API will return system collections as well which 207 | may not always be useful. 208 | ''' 209 | return [ col['collection'] for col in self.get('/collections') if not col['collection'].startswith('directus') ] 210 | 211 | def get_all_fk_fields(self, collection_name: str) -> dict: 212 | ''' 213 | Return all foreign key fields in the directus collection 214 | ''' 215 | return [ field for field in self.get(f"/fields/{collection_name}") 216 | if 'foreign_key_table' in field['schema'].keys() 217 | and field['schema']['foreign_key_table'] is not None 218 | ] 219 | 220 | def get_relations(self, collection_name: str) -> list: 221 | ''' 222 | Return all relations in a directus collection in a fixed format. All other keys are usually not necessary 223 | ''' 224 | return [{ 225 | "collection": relation["collection"], 226 | "field": relation["field"], 227 | "related_collection": relation["related_collection"] 228 | } for relation in self.get(f"/relations/{collection_name}")] 229 | 230 | def post_relation(self, relation: dict) -> None: 231 | ''' 232 | Keep posting if run into id not unique as sometimes relations are posted with ID, thus not triggering the 233 | auto-increment of the id field 234 | ''' 235 | assert set(relation.keys()) == set(['collection', 'field', 'related_collection']) 236 | try: 237 | self.post(f"/relations", json=relation) 238 | except AssertionError as e: 239 | if '"id" has to be unique' in str(e): 240 | self.post_relation(relation) 241 | else: 242 | raise 243 | -------------------------------------------------------------------------------- /coverage.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | /home/jason/projects/directus-sdk-python 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | --------------------------------------------------------------------------------