Skip to content

Commit 892af4c

Browse files
authored
Merge pull request #11 from JupiterOne/resolveConflictsMigrateRepo
Resolve conflicts migrate repo
2 parents 91c65d6 + a5866dc commit 892af4c

File tree

11 files changed

+546
-112
lines changed

11 files changed

+546
-112
lines changed

.travis.yml

Lines changed: 0 additions & 12 deletions
This file was deleted.

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2020 Auth0
3+
Copyright (c) 2024 JupiterOne
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python-370/)
55

66

7-
A Python library for the [JupiterOne API](https://support.jupiterone.io/hc/en-us/articles/360022722094-JupiterOne-Platform-API).
7+
A Python library for the [JupiterOne API](https://docs.jupiterone.io/reference).
88

99
## Installation
1010

@@ -41,6 +41,10 @@ query_result = j1.query_v1(QUERY, include_deleted=True)
4141
# Tree query
4242
QUERY = 'FIND Host RETURN TREE'
4343
query_result = j1.query_v1(QUERY)
44+
45+
# Using cursor graphQL variable to return full set of paginated results
46+
QUERY = "FIND (Device | Person)"
47+
cursor_query_r = j1._cursor_query(QUERY)
4448
```
4549

4650
##### Create an entity:

examples/examples.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from jupiterone.client import JupiterOneClient
2+
import random
3+
import time
4+
import os
5+
6+
account = os.environ.get("JUPITERONE_ACCOUNT")
7+
token = os.environ.get("JUPITERONE_TOKEN")
8+
9+
j1 = JupiterOneClient(account=account, token=token)
10+
11+
# query_v1
12+
q = "FIND jupiterone_user"
13+
query_r = j1.query_v1(q)
14+
print(query_r)
15+
16+
# create_entity
17+
num1 = random.randrange(1, 999, 1)
18+
19+
# create_entity
20+
properties = {
21+
'displayName': 'test{}'.format(num1),
22+
'customProperty': 'customVal',
23+
'tag.Production': 'false',
24+
'owner': 'user.name@jupiterone.com'
25+
}
26+
27+
create_r = j1.create_entity(
28+
entity_key='jupiterone-api-client-python:{}'.format(num1),
29+
entity_type='python_client_create_entity',
30+
entity_class='Record',
31+
properties=properties,
32+
timestamp=int(time.time()) * 1000 # Optional, defaults to current datetime
33+
)
34+
print(create_r)
35+
36+
properties = {
37+
'customProperty': 'customValUpdated'
38+
}
39+
40+
# update_entity
41+
update_r = j1.update_entity(
42+
entity_id='{}'.format(create_r['entity']['_id']),
43+
properties=properties
44+
)
45+
print(update_r)
46+
47+
# create_entity_2
48+
num2 = random.randrange(1, 999, 1)
49+
50+
properties = {
51+
'displayName': 'test{}'.format(num2),
52+
'customProperty': 'customVal',
53+
'tag.Production': 'false',
54+
'owner': 'user.name@jupiterone.com'
55+
}
56+
57+
create_r_2 = j1.create_entity(
58+
entity_key='jupiterone-api-client-python:{}'.format(num2),
59+
entity_type='python_client_create_entity',
60+
entity_class='Record',
61+
properties=properties,
62+
timestamp=int(time.time()) * 1000 # Optional, defaults to current datetime
63+
)
64+
print(create_r_2)
65+
66+
# create_relationship
67+
create_relationship_r = j1.create_relationship(
68+
relationship_key='{}:{}'.format(create_r['entity']['_id'], create_r_2['entity']['_id']),
69+
relationship_type='jupiterone-api-client-python:create_relationship',
70+
relationship_class='HAS',
71+
from_entity_id=create_r['entity']['_id'],
72+
to_entity_id=create_r_2['entity']['_id']
73+
)
74+
print(create_relationship_r)
75+
76+
# delete_relationship
77+
delete_relationship_r = j1.delete_relationship(relationship_id=create_relationship_r['relationship']['_id'])
78+
print(delete_relationship_r)
79+
80+
# delete_entity
81+
delete_entity_r1 = j1.delete_entity(entity_id=create_r['entity']['_id'])
82+
print(delete_entity_r1)
83+
84+
delete_entity_r2 = j1.delete_entity(entity_id=create_r_2['entity']['_id'])
85+
print(delete_entity_r2)
86+
87+
88+
q = "FIND (Device | Person)"
89+
cursor_query_r = j1._cursor_query(q)
90+
print(cursor_query_r)
91+
print(len(cursor_query_r['data']))

jupiterone/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
from .errors import (
33
JupiterOneClientError,
44
JupiterOneApiError
5-
)
5+
)

jupiterone/client.py

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import requests
99
from retrying import retry
10+
from warnings import warn
1011

1112
from jupiterone.errors import (
1213
JupiterOneClientError,
@@ -22,9 +23,11 @@
2223
DELETE_ENTITY,
2324
UPDATE_ENTITY,
2425
CREATE_RELATIONSHIP,
25-
DELETE_RELATIONSHIP
26+
DELETE_RELATIONSHIP,
27+
CURSOR_QUERY_V1
2628
)
2729

30+
2831
def retry_on_429(exc):
2932
""" Used to trigger retry on rate limit """
3033
return isinstance(exc, JupiterOneApiRetryError)
@@ -68,12 +71,12 @@ def account(self, value: str):
6871

6972
@property
7073
def token(self):
71-
""" Your JupiteOne access token """
74+
""" Your JupiterOne access token """
7275
return self._token
7376

7477
@token.setter
7578
def token(self, value: str):
76-
""" Your JupiteOne access token """
79+
""" Your JupiterOne access token """
7780
if not value:
7881
raise JupiterOneClientError('token is required')
7982
self._token = value
@@ -89,11 +92,11 @@ def _execute_query(self, query: str, variables: Dict = None) -> Dict:
8992
if variables:
9093
data.update(variables=variables)
9194

92-
response = requests.post(self.query_endpoint, headers=self.headers, json=data)
95+
response = requests.post(self.query_endpoint, headers=self.headers, json=data, timeout=60)
9396

9497
# It is still unclear if all responses will have a status
9598
# code of 200 or if 429 will eventually be used to
96-
# indicate rate limitting. J1 devs are aware.
99+
# indicate rate limits being hit. J1 devs are aware.
97100
if response.status_code == 200:
98101
if response._content:
99102
content = json.loads(response._content)
@@ -108,29 +111,59 @@ def _execute_query(self, query: str, variables: Dict = None) -> Dict:
108111
elif response.status_code == 401:
109112
raise JupiterOneApiError('401: Unauthorized. Please supply a valid account id and API token.')
110113

111-
elif response.status_code in [429, 500]:
114+
elif response.status_code in [429, 503]:
112115
raise JupiterOneApiRetryError('JupiterOne API rate limit exceeded')
113116

114-
else:
115-
try:
116-
content = json.loads(response._content)
117-
raise JupiterOneApiError('{}: {}'.format(response.status_code, content.get('error') or 'Unknown Error'))
118-
except ValueError as e:
119-
raise JupiterOneApiError('{}: {}'.format(response.status_code, 'Unknown Error'));
120-
117+
elif response.status_code in [500]:
118+
raise JupiterOneApiError('JupiterOne API internal server error.')
121119

122-
def query_v1(self, query: str, **kwargs) -> Dict:
123-
""" Performs a V1 graph query
120+
else:
121+
content = response._content
122+
if isinstance(content, (bytes, bytearray)):
123+
content = content.decode("utf-8")
124+
if 'application/json' in response.headers.get('Content-Type', 'text/plain'):
125+
data = json.loads(content)
126+
content = data.get('error', data.get('errors', content))
127+
raise JupiterOneApiError('{}:{}'.format(response.status_code, content))
128+
129+
def _cursor_query(self, query: str, cursor: str = None, include_deleted: bool = False) -> Dict:
130+
""" Performs a V1 graph query using cursor pagination
124131
args:
125132
query (str): Query text
126-
skip (int): Skip entity count
127-
limit (int): Limit entity count
133+
cursor (str): A pagination cursor for the initial query
128134
include_deleted (bool): Include recently deleted entities in query/search
129135
"""
130-
skip: int = kwargs.pop('skip', J1QL_SKIP_COUNT)
131-
limit: int = kwargs.pop('limit', J1QL_LIMIT_COUNT)
132-
include_deleted: bool = kwargs.pop('include_deleted', False)
133136

137+
results: List = []
138+
while True:
139+
variables = {
140+
'query': query,
141+
'includeDeleted': include_deleted
142+
}
143+
144+
if cursor is not None:
145+
variables['cursor'] = cursor
146+
147+
response = self._execute_query(query=CURSOR_QUERY_V1, variables=variables)
148+
data = response['data']['queryV1']['data']
149+
150+
if 'vertices' in data and 'edges' in data:
151+
return data
152+
153+
results.extend(data)
154+
155+
if 'cursor' in response['data']['queryV1'] and response['data']['queryV1']['cursor'] is not None:
156+
cursor = response['data']['queryV1']['cursor']
157+
else:
158+
break
159+
160+
return {'data': results}
161+
162+
def _limit_and_skip_query(self,
163+
query: str,
164+
skip: int = J1QL_SKIP_COUNT,
165+
limit: int = J1QL_LIMIT_COUNT,
166+
include_deleted: bool = False) -> Dict:
134167
results: List = []
135168
page: int = 0
136169

@@ -159,6 +192,39 @@ def query_v1(self, query: str, **kwargs) -> Dict:
159192

160193
return {'data': results}
161194

195+
def query_v1(self, query: str, **kwargs) -> Dict:
196+
""" Performs a V1 graph query
197+
args:
198+
query (str): Query text
199+
skip (int): Skip entity count
200+
limit (int): Limit entity count
201+
cursor (str): A pagination cursor for the initial query
202+
include_deleted (bool): Include recently deleted entities in query/search
203+
"""
204+
uses_limit_and_skip: bool = 'skip' in kwargs.keys() or 'limit' in kwargs.keys()
205+
skip: int = kwargs.pop('skip', J1QL_SKIP_COUNT)
206+
limit: int = kwargs.pop('limit', J1QL_LIMIT_COUNT)
207+
include_deleted: bool = kwargs.pop('include_deleted', False)
208+
cursor: str = kwargs.pop('cursor', None)
209+
210+
if uses_limit_and_skip:
211+
warn('limit and skip pagination is no longer a recommended method for pagination. '
212+
'To read more about using cursors checkout the JupiterOne documentation: '
213+
'https://docs.jupiterone.io/features/admin/parameters#query-parameterlist',
214+
DeprecationWarning, stacklevel=2)
215+
return self._limit_and_skip_query(
216+
query=query,
217+
skip=skip,
218+
limit=limit,
219+
include_deleted=include_deleted
220+
)
221+
else:
222+
return self._cursor_query(
223+
query=query,
224+
cursor=cursor,
225+
include_deleted=include_deleted
226+
)
227+
162228
def create_entity(self, **kwargs) -> Dict:
163229
""" Creates an entity in graph. It will also update an existing entity.
164230
@@ -206,7 +272,7 @@ def update_entity(self, entity_id: str = None, properties: Dict = None) -> Dict:
206272
Update an existing entity.
207273
208274
args:
209-
entity_id (str): The _id of the entity to udate
275+
entity_id (str): The _id of the entity to update
210276
properties (dict): Dictionary of key/value entity properties
211277
"""
212278
variables = {
@@ -218,7 +284,7 @@ def update_entity(self, entity_id: str = None, properties: Dict = None) -> Dict:
218284

219285
def create_relationship(self, **kwargs) -> Dict:
220286
"""
221-
Create a relationship (edge) between two entities (veritces).
287+
Create a relationship (edge) between two entities (vertices).
222288
223289
args:
224290
relationship_key (str): Unique key for the relationship

jupiterone/constants.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@
1010
}
1111
"""
1212

13+
CURSOR_QUERY_V1 = """
14+
query J1QL_v2($query: String!, $variables: JSON, $flags: QueryV1Flags, $includeDeleted: Boolean, $cursor: String) {
15+
queryV1(
16+
query: $query
17+
variables: $variables
18+
deferredResponse: DISABLED
19+
flags: $flags
20+
includeDeleted: $includeDeleted
21+
cursor: $cursor
22+
) {
23+
type
24+
data
25+
cursor
26+
__typename
27+
}
28+
}
29+
"""
30+
1331
CREATE_ENTITY = """
1432
mutation CreateEntity(
1533
$entityKey: String!

jupiterone/errors.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
class JupiterOneClientError(Exception):
33
""" Raised when error creating client """
44

5+
56
class JupiterOneApiRetryError(Exception):
67
""" Used to trigger retry on rate limit """
78

9+
810
class JupiterOneApiError(Exception):
9-
""" Raised when API returns error response """
11+
""" Raised when API returns error response """

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
retrying
22
requests
3+
warnings

setup.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
# Copyright (c) 2020 Auth0
2-
1+
# Copyright (c) 2020-2025 JupiterOne
32
from setuptools import setup, find_packages
43

54
install_reqs = [
65
'requests',
7-
'retrying'
6+
'retrying',
7+
'warnings'
88
]
99

1010
setup(name='jupiterone',
11-
version='0.1.0',
11+
version='1.0.0',
1212
description='A Python client for the JupiterOne API',
1313
license='MIT License',
14-
author='George Vauter',
15-
author_email='george.vauter@auth0.com',
16-
maintainer='Auth0',
17-
url='https://github.com/auth0/jupiterone-python-sdk',
14+
author='JupiterOne',
15+
author_email='solutions@jupiterone.com',
16+
maintainer='JupiterOne',
17+
url='https://github.com/JupiterOne/jupiterone-api-client-python',
1818
install_requires=install_reqs,
1919
classifiers=[
2020
'Development Status :: 4 - Beta',
@@ -28,4 +28,4 @@
2828
'Topic :: Security',
2929
],
3030
packages=find_packages()
31-
)
31+
)

0 commit comments

Comments
 (0)