├── tests ├── apis │ ├── __init__.py │ ├── conftest.py │ ├── cassettes │ │ ├── test_permissions │ │ │ ├── TestPermissions.test_get_user_permissions.yaml │ │ │ ├── TestPermissions.test_list_all_permissions_with_paging.yaml │ │ │ ├── TestPermissions.test_list_all_permissions.yaml │ │ │ ├── TestPermissions.test_add_user_permissions.yaml │ │ │ ├── TestPermissions.test_remove_user_permissions.yaml │ │ │ └── TestPermissions.test_update_user_permissions.yaml │ │ └── test_documents │ │ │ ├── TestDocuments.test_delete_documents.yaml │ │ │ ├── TestDocuments.test_index_documents_source_not_found.yaml │ │ │ ├── TestDocuments.test_delete_documents_source_not_found.yaml │ │ │ └── TestDocuments.test_index_documents.yaml │ ├── test_documents.py │ └── test_permissions.py ├── __init__.py ├── test_client.py └── test_request_session.py ├── MANIFEST.in ├── elastic_workplace_search ├── apis │ ├── __init__.py │ ├── permissions.py │ └── documents.py ├── __init__.py ├── __version__.py ├── client.py ├── exceptions.py ├── utils.py └── request_session.py ├── dev-requirements.txt ├── .ci ├── stop-stack.sh ├── start-stack.sh ├── README.md └── docker-compose.yml ├── setup.cfg ├── noxfile.py ├── CHANGELOG.md ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── setup.py ├── README.md └── LICENSE.txt /tests/apis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | 3 | -------------------------------------------------------------------------------- /elastic_workplace_search/apis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | mock 2 | pytest 3 | pytest-recording 4 | -------------------------------------------------------------------------------- /.ci/stop-stack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | # Stop all stack components 6 | docker-compose -f ./.ci/docker-compose.yml down --timeout 10 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # the inclusion of the tests module is not meant to offer best practices for 2 | # testing in general, but rather to support the `find_packages` example in 3 | # setup.py that excludes installing the "tests" package 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 6 | -------------------------------------------------------------------------------- /elastic_workplace_search/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import Client, Documents, Permissions, RequestSession 2 | from .__version__ import __version__ # noqa: F401 3 | 4 | __all__ = [ 5 | "Client", 6 | "Documents", 7 | "Permissions", 8 | "RequestSession", 9 | ] 10 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | from elastic_workplace_search.client import Client 2 | 3 | 4 | class TestClient: 5 | dummy_authorization_token = "authorization_token" 6 | 7 | def test_constructor(self): 8 | client = Client(self.dummy_authorization_token) 9 | assert isinstance(client, Client) 10 | -------------------------------------------------------------------------------- /elastic_workplace_search/__version__.py: -------------------------------------------------------------------------------- 1 | __title__ = "elastic_workplace_search" 2 | __description__ = "An API client for Elastic Workplace Search" 3 | __url__ = "https://github.com/elastic/workplace-search-python" 4 | __version__ = "0.3.0" 5 | __author__ = "Elastic" 6 | __author_email__ = "support@elastic.co" 7 | __maintainer__ = "Seth Michael Larson" 8 | __maintainer_email__ = "seth.larson@elastic.co" 9 | -------------------------------------------------------------------------------- /.ci/start-stack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | # Start stack components 6 | docker-compose -f ./.ci/docker-compose.yml up --detach elasticsearch enterprise-search 7 | 8 | # Wait until the product is up and running 9 | set +x 10 | echo -n 'Waiting for the stack to start (may take a while) .' 11 | until curl --silent --output /dev/null --max-time 1 http://localhost:8080/swiftype-app-version; do 12 | sleep 3; 13 | echo -n '.'; 14 | done 15 | 16 | echo '' 17 | -------------------------------------------------------------------------------- /tests/apis/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from elastic_workplace_search import Client 3 | 4 | 5 | @pytest.fixture(scope="session") 6 | def vcr_config(): 7 | return {"filter_headers": ["user-agent"]} 8 | 9 | 10 | @pytest.fixture(scope="session") 11 | def client(): 12 | return Client( 13 | authorization_token=( 14 | "32744aeb04f1269f57376347ee1d8f4e915e8273bfa9b2036aff4ef770bd2377" 15 | ), 16 | base_url="http://localhost:8080/api/ws/v1", 17 | ) 18 | 19 | 20 | @pytest.fixture(scope="session") 21 | def content_source_key(client): 22 | return "5eebbb1e5e21d6c1e64f9578" 23 | -------------------------------------------------------------------------------- /elastic_workplace_search/client.py: -------------------------------------------------------------------------------- 1 | from .request_session import RequestSession 2 | from .apis.documents import Documents 3 | from .apis.permissions import Permissions 4 | 5 | """API client for Elastic Workplace Search""" 6 | 7 | 8 | class Client: 9 | 10 | ELASTIC_WORKPLACE_SEARCH_BASE_URL = "http://localhost:3002/api/ws/v1" 11 | 12 | def __init__(self, authorization_token, base_url=ELASTIC_WORKPLACE_SEARCH_BASE_URL): 13 | self.authorization_token = authorization_token 14 | self.base_url = base_url 15 | self.session = RequestSession(self.authorization_token, self.base_url) 16 | 17 | self.documents = Documents(self.session) 18 | self.permissions = Permissions(self.session) 19 | -------------------------------------------------------------------------------- /elastic_workplace_search/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions raised by Elastic Workplace Search Client.""" 2 | 3 | 4 | class WorkplaceSearchError(Exception): 5 | """Base class for all Workplace Search errors.""" 6 | 7 | 8 | class InvalidCredentials(WorkplaceSearchError): 9 | """Raised when request cannot authenticate""" 10 | 11 | 12 | class NonExistentRecord(WorkplaceSearchError): 13 | """Raised when record does not exist""" 14 | 15 | 16 | class RecordAlreadyExists(WorkplaceSearchError): 17 | """Raised when record already exists""" 18 | 19 | 20 | class BadRequest(WorkplaceSearchError): 21 | """Raised when bad request""" 22 | 23 | 24 | class Forbidden(WorkplaceSearchError): 25 | """Raised when http forbidden""" 26 | -------------------------------------------------------------------------------- /.ci/README.md: -------------------------------------------------------------------------------- 1 | # CI 2 | 3 | ## Local Development 4 | 5 | You can currently get Enterprise Search running locally with docker-compose, 6 | run `docker-compose up` from this directory to get everything set up. 7 | You need to set the value of `STACK_VERSION` in your environment: 8 | 9 | ```bash 10 | $ STACK_VERSION=7.7.0 docker-compose up 11 | ``` 12 | 13 | You can check it's working with: 14 | ```bash 15 | curl http://localhost:8080/swiftype-app-version 16 | ``` 17 | 18 | or you can start everything and wait for it to become available 19 | with one bash script: 20 | 21 | ```bash 22 | $ STACK_VERSION=7.7.0 .ci/start-stack.sh 23 | ``` 24 | 25 | and stop everything when you're done: 26 | 27 | ```bash 28 | $ STACK_VERSION=7.7.0 .ci/stop-stack.sh 29 | ``` 30 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | 4 | SOURCE_FILES = ( 5 | "setup.py", 6 | "noxfile.py", 7 | "elastic_workplace_search/", 8 | "tests/", 9 | ) 10 | 11 | 12 | @nox.session(python=["2.7", "3.4", "3.5", "3.6", "3.7", "3.8"]) 13 | def test(session): 14 | session.install(".") 15 | session.install("-r", "dev-requirements.txt") 16 | 17 | session.run("pytest", "--record-mode=none", "tests/") 18 | 19 | 20 | @nox.session() 21 | def blacken(session): 22 | session.install("black") 23 | session.run("black", *SOURCE_FILES) 24 | 25 | lint(session) 26 | 27 | 28 | @nox.session() 29 | def lint(session): 30 | session.install("flake8", "black") 31 | session.run("black", "--check", *SOURCE_FILES) 32 | session.run("flake8", "--select=E,W,F", "--max-line-length=88", *SOURCE_FILES) 33 | -------------------------------------------------------------------------------- /elastic_workplace_search/utils.py: -------------------------------------------------------------------------------- 1 | import signal 2 | import platform 3 | from functools import wraps 4 | 5 | 6 | class Timeout: 7 | def __init__(self, exception_class, seconds=1, error_message="Timeout"): 8 | self.exception_class = exception_class 9 | self.seconds = seconds 10 | self.error_message = error_message 11 | 12 | def handle_timeout(self, signum, frame): 13 | raise self.exception_class(self.error_message) 14 | 15 | def __enter__(self): 16 | signal.signal(signal.SIGALRM, self.handle_timeout) 17 | signal.alarm(self.seconds) 18 | 19 | def __exit__(self, type, value, traceback): 20 | signal.alarm(0) 21 | 22 | 23 | def windows_incompatible(error_message=None): 24 | error_message = error_message or "This function is not supported on Windows." 25 | 26 | def decorator(f): 27 | @wraps(f) 28 | def decorated(*args, **kwargs): 29 | if platform.system() == "Windows": 30 | raise OSError(error_message) 31 | return f(*args, **kwargs) 32 | 33 | return decorated 34 | 35 | return decorator 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.0 (2020-02-11) 4 | 5 | - This release is compatible with Workplace Search v7.6 6 | - Changed the package name to `elastic-workplace-search` to follow the product 7 | name change ([Announcement](https://www.elastic.co/blog/elastic-enterprise-search-updates-for-7-6-0)) 8 | - Changed API path from `/api/v1/ent/` to `/api/ws/v1/`. If previously 9 | using a custom API endpoint you may need to update accordingly. 10 | 11 | ## 0.2.0 (2019-10-17) 12 | 13 | - Changed the `index_documents` and `delete_documents` 14 | API methods to be namespaced under `client.documents`. 15 | API calls will need to be updated accordingly. 16 | - Added support for the Permissions API ([#20](https://github.com/elastic/workplace-search-python/pull/20)) 17 | 18 | ## 0.1.0 (2019-08-19) 19 | 20 | - Changed "Swiftype" references to "Elastic" in the README and code 21 | - Changed the package name to `elastic-enterprise-search` 22 | - Changed versioning to be pre-release again (0.1.0) since we are not yet GA 23 | similar to App Search 24 | - Added analytics HTTP headers `Swiftype-X-Client` and `Swiftype-X-Client-Version` 25 | - Added Circle CI 26 | -------------------------------------------------------------------------------- /tests/apis/cassettes/test_permissions/TestPermissions.test_get_user_permissions.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - Bearer 32744aeb04f1269f57376347ee1d8f4e915e8273bfa9b2036aff4ef770bd2377 11 | Connection: 12 | - keep-alive 13 | method: GET 14 | uri: http://localhost:8080/api/ws/v1/sources/5eebbb1e5e21d6c1e64f9578/permissions/enterprise_search 15 | response: 16 | body: 17 | string: '{"user":"enterprise_search","permissions":["permission1"]}' 18 | headers: 19 | Cache-Control: 20 | - max-age=0, private, must-revalidate 21 | Content-Type: 22 | - application/json; charset=utf-8 23 | ETag: 24 | - W/"6cb0d74ff26d6f74253df3ee283a38dd" 25 | Server: 26 | - Jetty(9.2.29.v20191105) 27 | Transfer-Encoding: 28 | - chunked 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-Frame-Options: 32 | - SAMEORIGIN 33 | X-Request-Id: 34 | - bd1e3517-4395-4399-980f-b8dcfd883491 35 | X-Runtime: 36 | - '0.040057' 37 | X-XSS-Protection: 38 | - 1; mode=block 39 | status: 40 | code: 200 41 | message: OK 42 | version: 1 43 | -------------------------------------------------------------------------------- /elastic_workplace_search/apis/permissions.py: -------------------------------------------------------------------------------- 1 | class Permissions: 2 | def __init__(self, session): 3 | self.session = session 4 | 5 | def list_all_permissions(self, content_source_key, current=1, size=25): 6 | endpoint = "sources/{}/permissions".format(content_source_key) 7 | params = {"page[current]": current, "page[size]": size} 8 | return self.session.request("get", endpoint, params=params) 9 | 10 | def get_user_permissions(self, content_source_key, user): 11 | endpoint = "sources/{}/permissions/{}".format(content_source_key, user) 12 | return self.session.request("get", endpoint) 13 | 14 | def update_user_permissions(self, content_source_key, user, options): 15 | endpoint = "sources/{}/permissions/{}".format(content_source_key, user) 16 | return self.session.request("post", endpoint, json=options) 17 | 18 | def add_user_permissions(self, content_source_key, user, options): 19 | endpoint = "sources/{}/permissions/{}/add".format(content_source_key, user) 20 | return self.session.request("post", endpoint, json=options) 21 | 22 | def remove_user_permissions(self, content_source_key, user, options): 23 | endpoint = "sources/{}/permissions/{}/remove".format(content_source_key, user) 24 | return self.session.request("post", endpoint, json=options) 25 | -------------------------------------------------------------------------------- /tests/apis/cassettes/test_permissions/TestPermissions.test_list_all_permissions_with_paging.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - Bearer 32744aeb04f1269f57376347ee1d8f4e915e8273bfa9b2036aff4ef770bd2377 11 | Connection: 12 | - keep-alive 13 | method: GET 14 | uri: http://localhost:8080/api/ws/v1/sources/5eebbb1e5e21d6c1e64f9578/permissions?page%5Bcurrent%5D=2&page%5Bsize%5D=20 15 | response: 16 | body: 17 | string: '{"meta":{"page":{"current":2,"total_pages":1,"total_results":1,"size":20}},"results":[]}' 18 | headers: 19 | Cache-Control: 20 | - max-age=0, private, must-revalidate 21 | Content-Type: 22 | - application/json; charset=utf-8 23 | ETag: 24 | - W/"1b970cecdea704c6fa9ccfa2a27c8ca9" 25 | Server: 26 | - Jetty(9.2.29.v20191105) 27 | Transfer-Encoding: 28 | - chunked 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-Frame-Options: 32 | - SAMEORIGIN 33 | X-Request-Id: 34 | - 8c31388d-eebb-4ed6-bc2b-caccf5cbdf79 35 | X-Runtime: 36 | - '0.039516' 37 | X-XSS-Protection: 38 | - 1; mode=block 39 | status: 40 | code: 200 41 | message: OK 42 | version: 1 43 | -------------------------------------------------------------------------------- /tests/apis/cassettes/test_documents/TestDocuments.test_delete_documents.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '[1, "2"]' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - Bearer 32744aeb04f1269f57376347ee1d8f4e915e8273bfa9b2036aff4ef770bd2377 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '8' 15 | Content-Type: 16 | - application/json 17 | method: POST 18 | uri: http://localhost:8080/api/ws/v1/sources/5eebbb1e5e21d6c1e64f9578/documents/bulk_destroy 19 | response: 20 | body: 21 | string: '{"results":[{"id":1,"success":true},{"id":"2","success":true}]}' 22 | headers: 23 | Cache-Control: 24 | - max-age=0, private, must-revalidate 25 | Content-Type: 26 | - application/json; charset=utf-8 27 | ETag: 28 | - W/"77b44f941fd70bd4db8769a374c4408d" 29 | Server: 30 | - Jetty(9.2.29.v20191105) 31 | Transfer-Encoding: 32 | - chunked 33 | X-Content-Type-Options: 34 | - nosniff 35 | X-Frame-Options: 36 | - SAMEORIGIN 37 | X-Request-Id: 38 | - 95ce13bc-5e1d-46f1-aaca-6858d1d310c6 39 | X-Runtime: 40 | - '0.164300' 41 | X-XSS-Protection: 42 | - 1; mode=block 43 | status: 44 | code: 200 45 | message: OK 46 | version: 1 47 | -------------------------------------------------------------------------------- /tests/apis/cassettes/test_permissions/TestPermissions.test_list_all_permissions.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - Bearer 32744aeb04f1269f57376347ee1d8f4e915e8273bfa9b2036aff4ef770bd2377 11 | Connection: 12 | - keep-alive 13 | method: GET 14 | uri: http://localhost:8080/api/ws/v1/sources/5eebbb1e5e21d6c1e64f9578/permissions?page%5Bcurrent%5D=1&page%5Bsize%5D=25 15 | response: 16 | body: 17 | string: '{"meta":{"page":{"current":1,"total_pages":1,"total_results":1,"size":25}},"results":[{"user":"enterprise_search","permissions":["permission1"]}]}' 18 | headers: 19 | Cache-Control: 20 | - max-age=0, private, must-revalidate 21 | Content-Type: 22 | - application/json; charset=utf-8 23 | ETag: 24 | - W/"21372cc57fbf7b6f6d19643937e4eaaf" 25 | Server: 26 | - Jetty(9.2.29.v20191105) 27 | Transfer-Encoding: 28 | - chunked 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-Frame-Options: 32 | - SAMEORIGIN 33 | X-Request-Id: 34 | - 7225d807-7a02-4322-a48a-6c05669c3eb6 35 | X-Runtime: 36 | - '0.041343' 37 | X-XSS-Protection: 38 | - 1; mode=block 39 | status: 40 | code: 200 41 | message: OK 42 | version: 1 43 | -------------------------------------------------------------------------------- /tests/apis/cassettes/test_documents/TestDocuments.test_index_documents_source_not_found.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '[1, 2]' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - Bearer 32744aeb04f1269f57376347ee1d8f4e915e8273bfa9b2036aff4ef770bd2377 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '6' 15 | Content-Type: 16 | - application/json 17 | method: POST 18 | uri: http://localhost:8080/api/ws/v1/sources/bad-source-key/documents/bulk_create 19 | response: 20 | body: 21 | string: '' 22 | headers: 23 | Cache-Control: 24 | - no-cache 25 | Content-Security-Policy: 26 | - script-src 'nonce-e+nsspLfDiVaIGU5HixHxA==' 'strict-dynamic' 'self'; object-src 27 | 'none'; base-uri 'none'; frame-ancestors 'self'; 28 | Content-Type: 29 | - text/html 30 | Server: 31 | - Jetty(9.2.29.v20191105) 32 | Transfer-Encoding: 33 | - chunked 34 | X-Content-Type-Options: 35 | - nosniff 36 | X-Frame-Options: 37 | - SAMEORIGIN 38 | X-Request-Id: 39 | - 81b28e5a-56c3-46e3-820a-8e6238361ca6 40 | X-Runtime: 41 | - '0.036074' 42 | X-XSS-Protection: 43 | - 1; mode=block 44 | status: 45 | code: 404 46 | message: Not Found 47 | version: 1 48 | -------------------------------------------------------------------------------- /tests/apis/cassettes/test_documents/TestDocuments.test_delete_documents_source_not_found.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '[1, 2]' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - Bearer 32744aeb04f1269f57376347ee1d8f4e915e8273bfa9b2036aff4ef770bd2377 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '6' 15 | Content-Type: 16 | - application/json 17 | method: POST 18 | uri: http://localhost:8080/api/ws/v1/sources/bad-source-key/documents/bulk_destroy 19 | response: 20 | body: 21 | string: '' 22 | headers: 23 | Cache-Control: 24 | - no-cache 25 | Content-Security-Policy: 26 | - script-src 'nonce-L6XvPcXkNl85q8Wwyr0rVw==' 'strict-dynamic' 'self'; object-src 27 | 'none'; base-uri 'none'; frame-ancestors 'self'; 28 | Content-Type: 29 | - text/html 30 | Server: 31 | - Jetty(9.2.29.v20191105) 32 | Transfer-Encoding: 33 | - chunked 34 | X-Content-Type-Options: 35 | - nosniff 36 | X-Frame-Options: 37 | - SAMEORIGIN 38 | X-Request-Id: 39 | - 3e12a641-e82d-4639-8816-8dfa123d2200 40 | X-Runtime: 41 | - '0.020764' 42 | X-XSS-Protection: 43 | - 1; mode=block 44 | status: 45 | code: 404 46 | message: Not Found 47 | version: 1 48 | -------------------------------------------------------------------------------- /tests/apis/cassettes/test_permissions/TestPermissions.test_add_user_permissions.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"permissions": ["permission1"]}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - Bearer 32744aeb04f1269f57376347ee1d8f4e915e8273bfa9b2036aff4ef770bd2377 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '32' 15 | Content-Type: 16 | - application/json 17 | method: POST 18 | uri: http://localhost:8080/api/ws/v1/sources/5eebbb1e5e21d6c1e64f9578/permissions/enterprise_search/add 19 | response: 20 | body: 21 | string: '{"user":"enterprise_search","permissions":["permission1"]}' 22 | headers: 23 | Cache-Control: 24 | - max-age=0, private, must-revalidate 25 | Content-Type: 26 | - application/json; charset=utf-8 27 | ETag: 28 | - W/"6cb0d74ff26d6f74253df3ee283a38dd" 29 | Server: 30 | - Jetty(9.2.29.v20191105) 31 | Transfer-Encoding: 32 | - chunked 33 | X-Content-Type-Options: 34 | - nosniff 35 | X-Frame-Options: 36 | - SAMEORIGIN 37 | X-Request-Id: 38 | - 6cee13b3-2f2c-4983-9b35-cbe1825b9043 39 | X-Runtime: 40 | - '0.061162' 41 | X-XSS-Protection: 42 | - 1; mode=block 43 | status: 44 | code: 200 45 | message: OK 46 | version: 1 47 | -------------------------------------------------------------------------------- /tests/apis/test_documents.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from elastic_workplace_search.exceptions import NonExistentRecord 3 | 4 | 5 | class TestDocuments: 6 | @pytest.mark.vcr 7 | def test_index_documents(self, client, content_source_key): 8 | documents = [ 9 | {"id": 1, "url": "", "title": "", "body": ""}, 10 | {"id": "2", "url": "", "title": "", "body": ""}, 11 | ] 12 | 13 | resp = client.documents.index_documents(content_source_key, documents) 14 | assert list(resp) == ["results"] 15 | assert resp["results"] == [{"errors": [], "id": "1"}, {"errors": [], "id": "2"}] 16 | 17 | @pytest.mark.vcr 18 | def test_delete_documents(self, client, content_source_key): 19 | resp = client.documents.delete_documents(content_source_key, [1, "2"]) 20 | assert list(resp) == ["results"] 21 | assert resp["results"] == [ 22 | {"success": True, "id": 1}, 23 | {"success": True, "id": "2"}, 24 | ] 25 | 26 | @pytest.mark.vcr 27 | def test_index_documents_source_not_found(self, client): 28 | with pytest.raises(NonExistentRecord): 29 | client.documents.index_documents("bad-source-key", [1, 2]) 30 | 31 | @pytest.mark.vcr 32 | def test_delete_documents_source_not_found(self, client): 33 | with pytest.raises(NonExistentRecord): 34 | client.documents.delete_documents("bad-source-key", [1, 2]) 35 | -------------------------------------------------------------------------------- /tests/apis/cassettes/test_permissions/TestPermissions.test_remove_user_permissions.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"permissions": ["permission2"]}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - Bearer 32744aeb04f1269f57376347ee1d8f4e915e8273bfa9b2036aff4ef770bd2377 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '32' 15 | Content-Type: 16 | - application/json 17 | method: POST 18 | uri: http://localhost:8080/api/ws/v1/sources/5eebbb1e5e21d6c1e64f9578/permissions/enterprise_search/remove 19 | response: 20 | body: 21 | string: '{"user":"enterprise_search","permissions":["permission1"]}' 22 | headers: 23 | Cache-Control: 24 | - max-age=0, private, must-revalidate 25 | Content-Type: 26 | - application/json; charset=utf-8 27 | ETag: 28 | - W/"6cb0d74ff26d6f74253df3ee283a38dd" 29 | Server: 30 | - Jetty(9.2.29.v20191105) 31 | Transfer-Encoding: 32 | - chunked 33 | X-Content-Type-Options: 34 | - nosniff 35 | X-Frame-Options: 36 | - SAMEORIGIN 37 | X-Request-Id: 38 | - 77de7cbd-ede3-4074-9517-eb09330bc0bc 39 | X-Runtime: 40 | - '0.098598' 41 | X-XSS-Protection: 42 | - 1; mode=block 43 | status: 44 | code: 200 45 | message: OK 46 | version: 1 47 | -------------------------------------------------------------------------------- /tests/apis/cassettes/test_permissions/TestPermissions.test_update_user_permissions.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"permissions": ["permission1", "permission2"]}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - Bearer 32744aeb04f1269f57376347ee1d8f4e915e8273bfa9b2036aff4ef770bd2377 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '47' 15 | Content-Type: 16 | - application/json 17 | method: POST 18 | uri: http://localhost:8080/api/ws/v1/sources/5eebbb1e5e21d6c1e64f9578/permissions/enterprise_search 19 | response: 20 | body: 21 | string: '{"user":"enterprise_search","permissions":["permission1","permission2"]}' 22 | headers: 23 | Cache-Control: 24 | - max-age=0, private, must-revalidate 25 | Content-Type: 26 | - application/json; charset=utf-8 27 | ETag: 28 | - W/"b468c7da0c319e7f5f76f1b107306a23" 29 | Server: 30 | - Jetty(9.2.29.v20191105) 31 | Transfer-Encoding: 32 | - chunked 33 | X-Content-Type-Options: 34 | - nosniff 35 | X-Frame-Options: 36 | - SAMEORIGIN 37 | X-Request-Id: 38 | - 122e5566-6959-4680-aa04-2739aa4dec14 39 | X-Runtime: 40 | - '0.076391' 41 | X-XSS-Protection: 42 | - 1; mode=block 43 | status: 44 | code: 200 45 | message: OK 46 | version: 1 47 | -------------------------------------------------------------------------------- /tests/apis/cassettes/test_documents/TestDocuments.test_index_documents.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '[{"id": 1, "url": "", "title": "", "body": ""}, {"id": "2", "url": "", 4 | "title": "", "body": ""}]' 5 | headers: 6 | Accept: 7 | - '*/*' 8 | Accept-Encoding: 9 | - gzip, deflate 10 | Authorization: 11 | - Bearer 32744aeb04f1269f57376347ee1d8f4e915e8273bfa9b2036aff4ef770bd2377 12 | Connection: 13 | - keep-alive 14 | Content-Length: 15 | - '96' 16 | Content-Type: 17 | - application/json 18 | method: POST 19 | uri: http://localhost:8080/api/ws/v1/sources/5eebbb1e5e21d6c1e64f9578/documents/bulk_create 20 | response: 21 | body: 22 | string: '{"results":[{"id":"1","errors":[]},{"id":"2","errors":[]}]}' 23 | headers: 24 | Cache-Control: 25 | - max-age=0, private, must-revalidate 26 | Content-Type: 27 | - application/json; charset=utf-8 28 | ETag: 29 | - W/"524357b5bfe8f3cf4539eab2daa660ff" 30 | Server: 31 | - Jetty(9.2.29.v20191105) 32 | Transfer-Encoding: 33 | - chunked 34 | X-Content-Type-Options: 35 | - nosniff 36 | X-Frame-Options: 37 | - SAMEORIGIN 38 | X-Request-Id: 39 | - 279c7794-35e5-47ad-a5e0-3228fd8e031d 40 | X-Runtime: 41 | - '0.147254' 42 | X-XSS-Protection: 43 | - 1; mode=block 44 | status: 45 | code: 200 46 | message: OK 47 | version: 1 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-python@v2 10 | with: 11 | python-version: 3.7 12 | - name: Lint 13 | run: | 14 | python3.7 -m pip install nox 15 | nox -s lint 16 | 17 | test: 18 | env: 19 | ENDPOINT: http://localhost:8080/api/ws/v1 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | python-version: [2.7, 3.5, 3.6, 3.7, 3.8] 24 | 25 | runs-on: ubuntu-latest 26 | steps: 27 | # Checkout and Setup Python 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-python@v1 30 | with: 31 | python-version: 3.7 32 | - if: matrix.python-version != '3.7' 33 | uses: actions/setup-python@v1 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | 37 | # Setup Enterprise Search 38 | - name: Configure sysctl limits 39 | run: | 40 | sudo swapoff -a 41 | sudo sysctl -w vm.swappiness=1 42 | sudo sysctl -w fs.file-max=262144 43 | sudo sysctl -w vm.max_map_count=262144 44 | - name: Run Enterprise Search 45 | uses: elastic/elastic-github-actions/enterprise-search@master 46 | with: 47 | stack-version: 7.7.0 48 | 49 | # Run tests 50 | - name: Test 51 | run: | 52 | python3.7 -m pip install nox 53 | nox -s test-${{ matrix.python-version }} 54 | -------------------------------------------------------------------------------- /elastic_workplace_search/request_session.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import elastic_workplace_search 3 | from .exceptions import ( 4 | InvalidCredentials, 5 | NonExistentRecord, 6 | RecordAlreadyExists, 7 | BadRequest, 8 | Forbidden, 9 | ) 10 | 11 | 12 | class RequestSession: 13 | def __init__(self, authorization_token, base_url): 14 | self.authorization_token = authorization_token 15 | self.base_url = base_url 16 | self.session = requests.Session() 17 | 18 | headers = { 19 | "Authorization": "Bearer {}".format(self.authorization_token), 20 | "X-Swiftype-Client": "elastic-workplace-search-python", 21 | "X-Swiftype-Client-Version": elastic_workplace_search.__version__, 22 | } 23 | self.session.headers.update(headers) 24 | 25 | def raise_if_error(self, response): 26 | if response.status_code == requests.codes.unauthorized: 27 | raise InvalidCredentials(response.reason) 28 | elif response.status_code == requests.codes.bad: 29 | raise BadRequest() 30 | elif response.status_code == requests.codes.conflict: 31 | raise RecordAlreadyExists() 32 | elif response.status_code == requests.codes.not_found: 33 | raise NonExistentRecord() 34 | elif response.status_code == requests.codes.forbidden: 35 | raise Forbidden() 36 | 37 | response.raise_for_status() 38 | 39 | def request(self, http_method, endpoint, **kwargs): 40 | url = "{}/{}".format(self.base_url, endpoint) 41 | response = self.session.request(http_method, url, **kwargs) 42 | self.raise_if_error(response) 43 | return response.json() 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # Environments 82 | .env 83 | .venv 84 | env/ 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | .spyproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | 95 | # mkdocs documentation 96 | /site 97 | 98 | # mypy 99 | .mypy_cache/ 100 | 101 | doc/_* 102 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | from os import path 4 | 5 | base_dir = path.abspath(path.dirname(__file__)) 6 | 7 | # Get the long description from the README file 8 | with open(path.join(base_dir, "README.md"), encoding="utf-8") as f: 9 | long_description = f.read() 10 | 11 | about = {} 12 | with open( 13 | path.join(base_dir, "elastic_workplace_search/__version__.py"), encoding="utf-8" 14 | ) as f: 15 | exec(f.read(), about) 16 | 17 | setup( 18 | name=about["__title__"], 19 | version=about["__version__"], 20 | description=about["__description__"], 21 | long_description=long_description, 22 | long_description_content_type="text/markdown", 23 | url=about["__url__"], 24 | author=about["__author__"], 25 | author_email=about["__author_email__"], 26 | maintainer=about["__maintainer__"], 27 | maintainer_email=about["__maintainer_email__"], 28 | license="Apache-2.0", 29 | classifiers=[ 30 | "Development Status :: 4 - Beta", 31 | "Intended Audience :: Developers", 32 | "License :: OSI Approved :: Apache Software License", 33 | "Programming Language :: Python :: 2", 34 | "Programming Language :: Python :: 2.7", 35 | "Programming Language :: Python :: 3", 36 | "Programming Language :: Python :: 3.4", 37 | "Programming Language :: Python :: 3.5", 38 | "Programming Language :: Python :: 3.6", 39 | "Programming Language :: Python :: 3.7", 40 | "Programming Language :: Python :: 3.8", 41 | ], 42 | keywords="elastic workplace search api", 43 | packages=find_packages(exclude=["tests"]), 44 | install_requires=["requests"], 45 | python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", 46 | ) 47 | -------------------------------------------------------------------------------- /.ci/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | elasticsearch: 5 | image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION:-7.x-SNAPSHOT} 6 | environment: 7 | - "node.name=es-node" 8 | - "discovery.type=single-node" 9 | - "cluster.name=ent-search-docker-cluster" 10 | - "bootstrap.memory_lock=true" 11 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 12 | - "xpack.license.self_generated.type=trial" 13 | - "xpack.security.enabled=true" 14 | - "xpack.security.authc.api_key.enabled=true" 15 | - "ELASTIC_PASSWORD=changeme" 16 | - "action.auto_create_index=.ent-search-*-logs-*,-.ent-search-*,+*" 17 | ulimits: 18 | memlock: 19 | soft: -1 20 | hard: -1 21 | ports: 22 | - 9200:9200 23 | 24 | #------------------------------------------------------------------------------------------------- 25 | enterprise-search: 26 | image: docker.elastic.co/enterprise-search/enterprise-search:${STACK_VERSION:?missing revision for enterprise search} 27 | environment: 28 | - "ENT_SEARCH_DEFAULT_PASSWORD=itsnotcloudsearch" 29 | - "ent_search.listen_port=8080" 30 | - "ent_search.external_url=http://localhost:8080" 31 | - "ent_search.auth.source=standard" 32 | - "elasticsearch.host=http://elasticsearch:9200" 33 | - "allow_es_settings_modification=true" 34 | - "elasticsearch.username=elastic" 35 | - "elasticsearch.password=changeme" 36 | - "secret_management.encryption_keys=['testtesttest']" 37 | ports: 38 | - 8080:8080 39 | - 8081:8081 40 | depends_on: 41 | - elasticsearch 42 | entrypoint: /bin/bash -c "until curl -s -f -u elastic:changeme elasticsearch:9200/_license|grep -q trial; do sleep 2; done; /usr/local/bin/docker-entrypoint.sh" 43 | -------------------------------------------------------------------------------- /tests/test_request_session.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from requests.status_codes import codes 3 | from mock import MagicMock, patch 4 | 5 | import elastic_workplace_search 6 | from elastic_workplace_search.request_session import RequestSession 7 | from elastic_workplace_search.exceptions import InvalidCredentials 8 | 9 | 10 | class TestRequestSession: 11 | 12 | dummy_authorization_token = "authorization_token" 13 | 14 | def test_request_success(self): 15 | http = RequestSession(self.dummy_authorization_token, "base_url") 16 | 17 | expected_return = {"foo": "bar"} 18 | stubbed_return = MagicMock(status_code=codes.ok, json=lambda: expected_return) 19 | with patch("requests.Session.request", return_value=stubbed_return): 20 | resp = http.request("post", "http://doesnt.matter.org") 21 | 22 | assert resp == expected_return 23 | 24 | def test_headers_initialization(self): 25 | http = RequestSession(self.dummy_authorization_token, "base_url") 26 | headers = dict(http.session.headers.items()) 27 | version = elastic_workplace_search.__version__ 28 | 29 | assert headers == { 30 | "Accept": "*/*", 31 | "Accept-Encoding": "gzip, deflate", 32 | "Authorization": "Bearer authorization_token", 33 | "Connection": "keep-alive", 34 | "User-Agent": "python-requests/2.24.0", 35 | "X-Swiftype-Client": "elastic-workplace-search-python", 36 | "X-Swiftype-Client-Version": version, 37 | } 38 | 39 | def test_request_throw_error(self): 40 | http = RequestSession(self.dummy_authorization_token, "base_url") 41 | stubbed_return = MagicMock(status_code=codes.unauthorized) 42 | 43 | with patch("requests.Session.request", return_value=stubbed_return): 44 | with pytest.raises(InvalidCredentials): 45 | http.request("post", "http://doesnt.matter.org") 46 | -------------------------------------------------------------------------------- /tests/apis/test_permissions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class TestPermissions: 5 | @pytest.mark.vcr 6 | def test_list_all_permissions(self, client, content_source_key): 7 | resp = client.permissions.list_all_permissions(content_source_key) 8 | assert resp == { 9 | "meta": { 10 | "page": {"current": 1, "total_pages": 1, "total_results": 1, "size": 25} 11 | }, 12 | "results": [{"permissions": ["permission1"], "user": "enterprise_search"}], 13 | } 14 | 15 | @pytest.mark.vcr 16 | def test_list_all_permissions_with_paging(self, client, content_source_key): 17 | resp = client.permissions.list_all_permissions( 18 | content_source_key, current=2, size=20 19 | ) 20 | assert resp == { 21 | "meta": { 22 | "page": {"current": 2, "total_pages": 1, "total_results": 1, "size": 20} 23 | }, 24 | "results": [], 25 | } 26 | 27 | @pytest.mark.vcr 28 | def test_add_user_permissions(self, client, content_source_key): 29 | resp = client.permissions.add_user_permissions( 30 | content_source_key, 31 | user="enterprise_search", 32 | options={"permissions": ["permission1"]}, 33 | ) 34 | assert resp == {"user": "enterprise_search", "permissions": ["permission1"]} 35 | 36 | @pytest.mark.vcr 37 | def test_get_user_permissions(self, client, content_source_key): 38 | resp = client.permissions.get_user_permissions( 39 | content_source_key, user="enterprise_search" 40 | ) 41 | assert resp == {"user": "enterprise_search", "permissions": ["permission1"]} 42 | 43 | @pytest.mark.vcr 44 | def test_update_user_permissions(self, client, content_source_key): 45 | resp = client.permissions.update_user_permissions( 46 | content_source_key, 47 | user="enterprise_search", 48 | options={"permissions": ["permission1", "permission2"]}, 49 | ) 50 | assert resp == { 51 | "user": "enterprise_search", 52 | "permissions": ["permission1", "permission2"], 53 | } 54 | 55 | @pytest.mark.vcr 56 | def test_remove_user_permissions(self, client, content_source_key): 57 | resp = client.permissions.remove_user_permissions( 58 | content_source_key, 59 | user="enterprise_search", 60 | options={"permissions": ["permission2"]}, 61 | ) 62 | assert resp == {"permissions": ["permission1"], "user": "enterprise_search"} 63 | -------------------------------------------------------------------------------- /elastic_workplace_search/apis/documents.py: -------------------------------------------------------------------------------- 1 | class Documents: 2 | def __init__(self, session): 3 | self.session = session 4 | 5 | def index_documents(self, content_source_key, documents, **kwargs): 6 | """Index a batch of documents in a content source. 7 | Raises :class:`~elastic_workplace_search.NonExistentRecord` if the 8 | content_source_key is malformed or invalid. Raises 9 | :class:`~elastic_workplace_search.WorkplaceSearchError` if there are any 10 | HTTP errors. 11 | 12 | :param content_source_key: Key for the content source. 13 | :param documents: Array of documents to be indexed. 14 | :return: Array of document indexing results. 15 | 16 | >>> from elastic_workplace_search import Client 17 | >>> from elastic_workplace_search.exceptions import WorkplaceSearchError 18 | >>> content_source_key = 'content source key' 19 | >>> authorization_token = 'authorization token' 20 | >>> client = Client(authorization_token) 21 | >>> documents = [ 22 | { 23 | 'id': '1', 24 | 'url': 'https://github.com/elastic/workplace-search-python', 25 | 'title': 'Elastic Workplace Search Official Python client', 26 | 'body': 'A descriptive body' 27 | } 28 | ] 29 | >>> try: 30 | >>> document_results = client.documents.index_documents( 31 | ... content_source_key, documents 32 | ... ) 33 | >>> print(document_results) 34 | >>> except WorkplaceSearchError: 35 | >>> # handle exception 36 | >>> pass 37 | [{'errors': [], 'id': '1', 'id': None}] 38 | """ 39 | return self._async_create_or_update_documents(content_source_key, documents) 40 | 41 | def delete_documents(self, content_source_key, ids): 42 | """Destroys documents in a content source by their ids. 43 | Raises :class:`~elastic_workplace_search.NonExistentRecord` if the 44 | content_source_key is malformed or invalid. Raises 45 | :class:`~elastic_workplace_search.WorkplaceSearchError` if there are any 46 | HTTP errors. 47 | 48 | :param content_source_key: Key for the content source. 49 | :param ids: Array of document ids to be destroyed. 50 | :return: Array of result dicts, with keys of `id` and `status` 51 | 52 | >>> from elastic_workplace_search import Client 53 | >>> from elastic_workplace_search.exceptions import WorkplaceSearchError 54 | >>> content_source_key = 'content source key' 55 | >>> authorization_token = 'authorization token' 56 | >>> client = Client(authorization_token) 57 | >>> try: 58 | >>> response = client.documents.delete_documents(content_source_key, ['1']) 59 | >>> print(response) 60 | >>> except WorkplaceSearchError: 61 | >>> # handle exception 62 | >>> pass 63 | [{"id": '1',"success": True}] 64 | """ 65 | endpoint = "sources/{}/documents/bulk_destroy".format(content_source_key) 66 | return self.session.request("post", endpoint, json=ids) 67 | 68 | def _async_create_or_update_documents(self, content_source_key, documents): 69 | endpoint = "sources/{}/documents/bulk_create".format(content_source_key) 70 | return self.session.request("post", endpoint, json=documents) 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **⚠️ This client is deprecated ⚠️** 2 | > 3 | > As of Enterprise Search version 7.10.0, we are directing users to the new [Enterprise Search Python Client](https://github.com/elastic/enterprise-search-python) and 4 | > deprecating this client. 5 | > 6 | > This client will be compatible with all Enterprise Search 7.x releases, but will not be compatible with 8.x releases. Our development effort on this project will 7 | > be limited to bug fixes. All future enhancements will be focused on the Enterprise Search Python Client. 8 | > 9 | > Thank you! - Elastic 10 | 11 |

GitHub Actions 12 | 13 | > A first-party Python client for [Elastic Workplace Search](https://www.elastic.co/workplace-search). 14 | 15 | ## Contents 16 | 17 | + [Getting started](#getting-started-) 18 | + [Usage](#usage) 19 | + [FAQ](#faq-) 20 | + [Contribute](#contribute-) 21 | + [License](#license-) 22 | 23 | *** 24 | 25 | ## Getting started 🐣 26 | 27 | Supports Python 2.7 and Python 3.4+. 28 | 29 | Installed with 30 | `pip `: 31 | 32 | ```bash 33 | $ python -m pip install elastic_workplace_search 34 | ``` 35 | 36 | You can also download and install the project source: 37 | 38 | ```bash 39 | $ python setup.py install 40 | ``` 41 | 42 | Depends on [requests](https://github.com/requests/requests) for making HTTP requests. 43 | 44 | ## Usage 45 | 46 | ### Creating a new Client 47 | 48 | ```python 49 | from elastic_workplace_search import Client 50 | authorization_token = 'authorization token' 51 | client = Client(authorization_token) 52 | ``` 53 | 54 | Retrieve your access token and a content source key after creating your content source. 55 | 56 | ### Change API endpoint 57 | 58 | ```python 59 | client = Client(authorization_token, "https://your-server.example.com/api/ws/v1") 60 | ``` 61 | 62 | ### Custom Source Documents 63 | 64 | Document API features are found in the `client.documents` module. 65 | 66 | #### Indexing Documents 67 | 68 | Indexing a document into a custom content source: 69 | 70 | ```python 71 | content_source_key = 'content source key' 72 | documents = [ 73 | { 74 | 'id': '1234', 75 | 'url': 'https://github.com/elastic/workplace-search-python', 76 | 'title': 'Elastic Workplace Search Official Python Client', 77 | 'body': 'A descriptive body, with document contents and metadata' 78 | } 79 | ] 80 | 81 | client.documents.index_documents(content_source_key, documents) 82 | ``` 83 | 84 | #### Deleting Documents 85 | 86 | Deleting a document from a custom content source: 87 | 88 | ```python 89 | content_source_key = 'content source key' 90 | ids = ['1234'] 91 | 92 | client.documents.delete_documents(content_source_key, ids) 93 | ``` 94 | 95 | ### Permissions 96 | 97 | Permissions API features are found in the `client.permissions` module. 98 | 99 | #### Listing all permissions 100 | 101 | ```python 102 | content_source_key = 'content source key' 103 | 104 | client.permissions.list_all_permissions(content_source_key) 105 | ``` 106 | 107 | #### Listing all permissions with paging 108 | 109 | ```python 110 | content_source_key = 'content source key' 111 | 112 | client.permissions.list_all_permissions(content_source_key, size=20, current=2) 113 | ``` 114 | 115 | #### Retrieve a User's permissions 116 | 117 | ```python 118 | content_source_key = 'content source key' 119 | user = 'enterprise_search' 120 | 121 | client.permissions.get_user_permissions(content_source_key, user) 122 | ``` 123 | 124 | #### Add permissions to a User 125 | 126 | ```python 127 | content_source_key = 'content source key' 128 | user = 'enterprise_search' 129 | permissions = ['permission1'] 130 | 131 | client.permissions.add_user_permissions(content_source_key, 'enterprise_search', { 'permissions': permissions }) 132 | ``` 133 | 134 | #### Update a User's permissions 135 | 136 | ```python 137 | content_source_key = 'content source key' 138 | user = 'enterprise_search' 139 | permissions = ['permission2'] 140 | 141 | client.permissions.update_user_permissions(content_source_key, 'enterprise_search', { 'permissions': permissions }) 142 | ``` 143 | 144 | #### Remove permissions from a User 145 | 146 | ```python 147 | content_source_key = 'content source key' 148 | user = 'enterprise_search' 149 | permissions = ['permission2'] 150 | 151 | client.permissions.remove_user_permissions(content_source_key, 'enterprise_search', { 'permissions': permissions }) 152 | ``` 153 | 154 | ## FAQ 🔮 155 | 156 | ### Where do I report issues with the client? 157 | 158 | If something is not working as expected, please open an [issue](https://github.com/elastic/workplace-search-python/issues/new). 159 | 160 | ## Contribute 🚀 161 | 162 | We welcome contributors to the project. Before you begin, a couple notes... 163 | 164 | + Before opening a pull request, please create an issue to [discuss the scope of your proposal](https://github.com/elastic/workplace-search-python/issues). 165 | + Please write simple code and concise documentation, when appropriate. 166 | 167 | ## License 📗 168 | 169 | [Apache 2.0](https://github.com/elastic/workplace-search-python/blob/master/LICENSE.txt) © [Elastic](https://github.com/elastic) 170 | 171 | Thank you to all the [contributors](https://github.com/elastic/workplace-search-python/graphs/contributors)! 172 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Elasticsearch BV 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------