├── 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 Python SDK
3 |
4 |
5 | [](https://github.com/Jason-CKY/directus-sdk-python/actions/workflows/test.yaml) 
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 |
--------------------------------------------------------------------------------