Merge branch 'develop'
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
config.ini
|
config.ini
|
||||||
.venv/
|
.venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
/config-mira.ini
|
||||||
|
/config-mira.json
|
||||||
|
|||||||
Generated
+2
@@ -3,6 +3,8 @@
|
|||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
<exclude-output />
|
<exclude-output />
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/integration_tests" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Python 3.10 (ActiveCollabAPI)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.10 (ActiveCollabAPI)" jdkType="Python SDK" />
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Integrationtests" type="tests" factoryName="Unittests">
|
||||||
|
<module name="ActiveCollabAPI" />
|
||||||
|
<option name="ENV_FILES" value="" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<option name="SDK_HOME" value="$PROJECT_DIR$/.venv/bin/python" />
|
||||||
|
<option name="SDK_NAME" value="Python 3.10 (ActiveCollabAPI)" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="" />
|
||||||
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
|
<option name="_new_pattern" value="""" />
|
||||||
|
<option name="_new_additionalArguments" value=""discover integration_tests"" />
|
||||||
|
<option name="_new_target" value="""" />
|
||||||
|
<option name="_new_targetType" value=""PATH"" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
Generated
+20
@@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Unittests" type="tests" factoryName="Unittests">
|
||||||
|
<module name="ActiveCollabAPI" />
|
||||||
|
<option name="ENV_FILES" value="" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<option name="SDK_HOME" value="$PROJECT_DIR$/.venv/bin/python" />
|
||||||
|
<option name="SDK_NAME" value="Python 3.10 (ActiveCollabAPI)" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="" />
|
||||||
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
|
<option name="_new_pattern" value="""" />
|
||||||
|
<option name="_new_additionalArguments" value=""discover tests"" />
|
||||||
|
<option name="_new_target" value="""" />
|
||||||
|
<option name="_new_targetType" value=""PATH"" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# system libs
|
|
||||||
import json
|
|
||||||
|
|
||||||
# 3rd party libs
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from ActiveCollabAPI import AC_LOGIN_BASE_URL, AC_USER_AGENT
|
|
||||||
|
|
||||||
|
|
||||||
class ACAuthenticator:
|
|
||||||
|
|
||||||
base_url = AC_LOGIN_BASE_URL
|
|
||||||
|
|
||||||
user = None
|
|
||||||
accounts = None
|
|
||||||
|
|
||||||
def headers(self):
|
|
||||||
return {
|
|
||||||
'Content-Type': 'application/json; charset=utf8',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'User-Agent': AC_USER_AGENT
|
|
||||||
}
|
|
||||||
|
|
||||||
def login(self, email, password):
|
|
||||||
login_data = {
|
|
||||||
'email': email,
|
|
||||||
'password': password
|
|
||||||
}
|
|
||||||
res = requests.post(self.base_url,
|
|
||||||
data=json.dumps(login_data),
|
|
||||||
headers=self.headers())
|
|
||||||
if res.status_code != 200:
|
|
||||||
raise Exception('Login failed!')
|
|
||||||
|
|
||||||
login_res = res.json()
|
|
||||||
if login_res['is_ok'] != 1:
|
|
||||||
raise Exception('Login failed! (2)')
|
|
||||||
|
|
||||||
self.user = login_res['user']
|
|
||||||
self.accounts = login_res['accounts']
|
|
||||||
return login_res
|
|
||||||
|
|
||||||
def find_account(self, value, key='user_display_name'):
|
|
||||||
found = list(filter(lambda a: a[key] == value,
|
|
||||||
self.accounts))
|
|
||||||
if len(found) == 0:
|
|
||||||
raise Exception("Account '%s=%s' not found!" % (key, value))
|
|
||||||
return found[0]
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from ActiveCollabAPI import AC_USER_AGENT, AC_API_VERSION, AC_API_CLIENT_NAME, AC_API_CLIENT_VENDOR
|
|
||||||
|
|
||||||
|
|
||||||
class ACTokenAuthenticator:
|
|
||||||
|
|
||||||
account = None
|
|
||||||
user = None
|
|
||||||
base_url = ""
|
|
||||||
token = None
|
|
||||||
|
|
||||||
def __init__(self, account, user):
|
|
||||||
self.account = account
|
|
||||||
self.user = user
|
|
||||||
#
|
|
||||||
self.base_url = self.account['url'] + '/api/v%s' % AC_API_VERSION
|
|
||||||
|
|
||||||
def issue_token_intent(self):
|
|
||||||
# use intent to get a token
|
|
||||||
data = {
|
|
||||||
'intent': self.user['intent'],
|
|
||||||
'client_name': AC_API_CLIENT_NAME,
|
|
||||||
'client_vendor': AC_API_CLIENT_VENDOR
|
|
||||||
}
|
|
||||||
r = requests.post(self.base_url+ '/issue-token-intent',
|
|
||||||
headers=self.headers(),
|
|
||||||
data=json.dumps(data))
|
|
||||||
if r.status_code != 200:
|
|
||||||
raise Exception('Error in issuing token intent!')
|
|
||||||
token_res = r.json()
|
|
||||||
if token_res['is_ok'] != 1:
|
|
||||||
raise Exception('Error in issuing token (2)')
|
|
||||||
return token_res['token']
|
|
||||||
|
|
||||||
def headers(self):
|
|
||||||
return {
|
|
||||||
'Content-Type': 'application/json; charset=utf8',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'User-Agent': AC_USER_AGENT
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AcAccount:
|
||||||
|
name: int
|
||||||
|
url: str
|
||||||
|
display_name: str
|
||||||
|
user_display_name: str
|
||||||
|
position: int
|
||||||
|
class_: str
|
||||||
|
status: str
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
d = dataclasses.asdict(self)
|
||||||
|
d["class"] = d["class_"]
|
||||||
|
del d["class_"]
|
||||||
|
return d
|
||||||
|
|
||||||
|
def to_json(self) -> json:
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
|
|
||||||
|
def account_from_json(a) -> AcAccount:
|
||||||
|
a["class_"] = a["class"]
|
||||||
|
del a["class"]
|
||||||
|
return AcAccount(**a)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# system libs
|
||||||
|
import json
|
||||||
|
|
||||||
|
# 3rd party libs
|
||||||
|
import requests
|
||||||
|
from requests import Response
|
||||||
|
|
||||||
|
from ActiveCollabAPI import AC_USER_AGENT
|
||||||
|
|
||||||
|
|
||||||
|
class AcAuthenticator:
|
||||||
|
|
||||||
|
base_url: str = ""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str):
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
def headers(self):
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json; charset=utf8',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': AC_USER_AGENT
|
||||||
|
}
|
||||||
|
|
||||||
|
def login(self, email: str, password: str) -> Response:
|
||||||
|
login_data = {
|
||||||
|
'email': email,
|
||||||
|
'password': password
|
||||||
|
}
|
||||||
|
return requests.post(self.base_url,
|
||||||
|
data=json.dumps(login_data),
|
||||||
|
headers=self.headers())
|
||||||
@@ -3,24 +3,30 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from ActiveCollabAPI import AC_USER_AGENT, AC_API_VERSION
|
from ActiveCollabAPI import AC_USER_AGENT, AC_API_VERSION
|
||||||
|
from ActiveCollabAPI.AcAccount import AcAccount
|
||||||
|
from ActiveCollabAPI.AcSubtask import AcSubtask
|
||||||
|
from ActiveCollabAPI.AcToken import AcToken
|
||||||
|
|
||||||
|
|
||||||
class ACClient:
|
class AcClient:
|
||||||
|
"""
|
||||||
|
Active Collab REST API Client
|
||||||
|
"""
|
||||||
base_url = None
|
base_url = None
|
||||||
account = None
|
account = None
|
||||||
token = None
|
token = None
|
||||||
|
|
||||||
def __init__(self, account, token):
|
def __init__(self, account: AcAccount, token: AcToken):
|
||||||
self.account = account
|
self.account = account
|
||||||
self.token = token
|
self.token = token
|
||||||
#
|
#
|
||||||
self.base_url = self.account['url'] + '/api/v%d' % AC_API_VERSION
|
self.base_url = self.account.url + '/api/v%d' % AC_API_VERSION
|
||||||
|
|
||||||
def headers(self):
|
def headers(self):
|
||||||
return {
|
return {
|
||||||
'Content-Type': 'application/json; charset=utf8',
|
'Content-Type': 'application/json; charset=utf8',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'X-Angie-AuthApiToken': self.token,
|
'X-Angie-AuthApiToken': self.token.token,
|
||||||
'User-Agent': AC_USER_AGENT
|
'User-Agent': AC_USER_AGENT
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,12 +96,12 @@ class ACClient:
|
|||||||
return self._delete('projects/%d/tasks/%s/subtasks/%s' %
|
return self._delete('projects/%d/tasks/%s/subtasks/%s' %
|
||||||
(project_id, task_id, subtask_id))
|
(project_id, task_id, subtask_id))
|
||||||
|
|
||||||
def create_subtask(self, subtask):
|
def create_subtask(self, subtask: AcSubtask):
|
||||||
project_id = subtask['project_id']
|
project_id = subtask.project_id
|
||||||
task_id = subtask['task_id']
|
task_id = subtask.task_id
|
||||||
return self._post('projects/%d/tasks/%s/subtasks' %
|
return self._post('projects/%d/tasks/%s/subtasks' %
|
||||||
(project_id, task_id),
|
(project_id, task_id),
|
||||||
data=subtask)
|
data=subtask.to_dict())
|
||||||
|
|
||||||
# find subtask with matching body
|
# find subtask with matching body
|
||||||
def find_subtask_with_body(self, task, body, is_completed=False):
|
def find_subtask_with_body(self, task, body, is_completed=False):
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from ActiveCollabAPI.AcUser import AcUser
|
||||||
|
from ActiveCollabAPI.AcAccount import AcAccount
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AcLoginResponse:
|
||||||
|
user: AcUser
|
||||||
|
accounts: list[AcAccount]
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AcProject:
|
||||||
|
based_on_id: int
|
||||||
|
based_on_type: str
|
||||||
|
body: str
|
||||||
|
body_formatted: str
|
||||||
|
budget: float
|
||||||
|
budget_type: str
|
||||||
|
budgeting_interval: str
|
||||||
|
category_id: int
|
||||||
|
class_: str
|
||||||
|
company_id: int
|
||||||
|
completed_by_id: int
|
||||||
|
completed_on: int
|
||||||
|
count_discussions: int
|
||||||
|
count_files: int
|
||||||
|
count_notes: int
|
||||||
|
count_tasks: int
|
||||||
|
created_by_email: str
|
||||||
|
created_by_id: int
|
||||||
|
created_by_name: str
|
||||||
|
created_on: int
|
||||||
|
currency_id: int
|
||||||
|
email: str
|
||||||
|
file_size: int
|
||||||
|
id: int
|
||||||
|
is_billable: bool
|
||||||
|
is_client_reporting_enabled: bool
|
||||||
|
is_completed: bool
|
||||||
|
is_estimate_visible_to_subcontractors: bool
|
||||||
|
is_sample: bool
|
||||||
|
is_tracking_enabled: bool
|
||||||
|
is_trashed: bool
|
||||||
|
label_id: int
|
||||||
|
last_activity_on: int
|
||||||
|
leader_id: int
|
||||||
|
members: list[int]
|
||||||
|
members_can_change_billable: bool
|
||||||
|
name: str
|
||||||
|
project_number: int
|
||||||
|
trashed_by_id: int
|
||||||
|
trashed_on: int
|
||||||
|
updated_by_id: int
|
||||||
|
updated_on: int
|
||||||
|
url_path: str
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
d = dataclasses.asdict(self)
|
||||||
|
d["class"] = d["class_"]
|
||||||
|
del d["class_"]
|
||||||
|
return d
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
def project_from_json(json_obj: dict) -> AcProject:
|
||||||
|
json_obj["class_"] = json_obj["class"]
|
||||||
|
del json_obj["class"]
|
||||||
|
return AcProject(**json_obj)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from ActiveCollabAPI.AcAccount import AcAccount
|
||||||
|
from ActiveCollabAPI.AcToken import AcToken
|
||||||
|
from ActiveCollabAPI.AcUser import AcUser
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AcSession:
|
||||||
|
user: AcUser
|
||||||
|
accounts: [AcAccount]
|
||||||
|
cur_account: AcAccount
|
||||||
|
token: AcToken
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
d = dataclasses.asdict(self)
|
||||||
|
d["accounts"] = list(map(lambda a: a.to_dict(), self.accounts))
|
||||||
|
return d
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AcSubtask:
|
||||||
|
task_id: int
|
||||||
|
project_id: int
|
||||||
|
name: str = None
|
||||||
|
body: str = None
|
||||||
|
assignee_id: int = 0
|
||||||
|
delegated_by_id: int = 0
|
||||||
|
created_on: int = None
|
||||||
|
created_by_id: int = None
|
||||||
|
created_by_name: str = None
|
||||||
|
created_by_email: str = None
|
||||||
|
url_path: str = ""
|
||||||
|
id: int = 0
|
||||||
|
due_on: int = 0
|
||||||
|
fake_assignee_name: str = None
|
||||||
|
fake_assignee_email: str = None
|
||||||
|
class_: str = "Subtask"
|
||||||
|
completed_by_id: int = 0
|
||||||
|
is_completed: bool = False
|
||||||
|
is_trashed: bool = False
|
||||||
|
trashed_on: int = 0
|
||||||
|
trashed_by_id: int = 0
|
||||||
|
updated_on: int = 0
|
||||||
|
position: int = 0
|
||||||
|
completed_on: int = 0
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
d = dataclasses.asdict(self)
|
||||||
|
d["class"] = d["class_"]
|
||||||
|
del d["class_"]
|
||||||
|
return d
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
def subtask_from_json(json_obj: dict) -> AcSubtask:
|
||||||
|
json_obj["class_"] = json_obj["class"]
|
||||||
|
del json_obj["class"]
|
||||||
|
return AcSubtask(**json_obj)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AcToken:
|
||||||
|
token: str
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests import Response
|
||||||
|
|
||||||
|
from ActiveCollabAPI import AC_USER_AGENT, AC_API_VERSION, AC_API_CLIENT_NAME, AC_API_CLIENT_VENDOR
|
||||||
|
|
||||||
|
|
||||||
|
class AcTokenAuthenticator:
|
||||||
|
|
||||||
|
base_url = ""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str):
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
|
||||||
|
def headers(self):
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json; charset=utf8',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': AC_USER_AGENT
|
||||||
|
}
|
||||||
|
|
||||||
|
def issue_token_intent(self, intent: str) -> Response:
|
||||||
|
# use intent to get a token
|
||||||
|
data = {
|
||||||
|
'intent': intent,
|
||||||
|
'client_name': AC_API_CLIENT_NAME,
|
||||||
|
'client_vendor': AC_API_CLIENT_VENDOR
|
||||||
|
}
|
||||||
|
return requests.post(self.base_url+ '/issue-token-intent',
|
||||||
|
headers=self.headers(),
|
||||||
|
data=json.dumps(data))
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AcUser:
|
||||||
|
avatar_url: str
|
||||||
|
first_name: str
|
||||||
|
last_name: str
|
||||||
|
intent: str
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
d = dataclasses.asdict(self)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
|
||||||
|
from ActiveCollabAPI import AC_API_VERSION
|
||||||
|
from ActiveCollabAPI.AcAuthenticator import AcAuthenticator
|
||||||
|
from ActiveCollabAPI.AcClient import AcClient
|
||||||
|
from ActiveCollabAPI.AcTokenAuthenticator import AcTokenAuthenticator
|
||||||
|
from ActiveCollabAPI.AcAccount import AcAccount, account_from_json
|
||||||
|
from ActiveCollabAPI.AcLoginResponse import AcLoginResponse
|
||||||
|
from ActiveCollabAPI.AcProject import AcProject, project_from_json
|
||||||
|
from ActiveCollabAPI.AcSession import AcSession
|
||||||
|
from ActiveCollabAPI.AcSubtask import AcSubtask, subtask_from_json
|
||||||
|
from ActiveCollabAPI.AcToken import AcToken
|
||||||
|
from ActiveCollabAPI.AcUser import AcUser
|
||||||
|
|
||||||
|
|
||||||
|
class ActiveCollab:
|
||||||
|
"""
|
||||||
|
Active Collab Client library comming from the use case
|
||||||
|
"""
|
||||||
|
base_url: str = ""
|
||||||
|
|
||||||
|
session: AcSession = None
|
||||||
|
|
||||||
|
def __init__(self, base_url: str):
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
def login_to_first_account(self, email: str, password: str) -> AcSession:
|
||||||
|
login_res = self.user_login(email, password)
|
||||||
|
cur_account = self.select_first_account(login_res.accounts)
|
||||||
|
token = self.create_token(cur_account, login_res.user)
|
||||||
|
self.session = AcSession(login_res.user, login_res.accounts, cur_account, token)
|
||||||
|
return self.session
|
||||||
|
|
||||||
|
def user_login(self, email: str, password: str) -> AcLoginResponse:
|
||||||
|
auth = AcAuthenticator(self.base_url)
|
||||||
|
res = auth.login(email, password)
|
||||||
|
if res.status_code != 200:
|
||||||
|
raise Exception('Login failed!')
|
||||||
|
res_data = res.json()
|
||||||
|
if res_data['is_ok'] != 1:
|
||||||
|
raise Exception('Login failed! (2)')
|
||||||
|
accounts = list(map(lambda a: account_from_json(a), res_data['accounts']))
|
||||||
|
return AcLoginResponse(AcUser(**res_data['user']), accounts)
|
||||||
|
|
||||||
|
def select_first_account(self, accounts: list[AcAccount]) -> AcAccount:
|
||||||
|
return accounts[0]
|
||||||
|
|
||||||
|
def create_token(self, account: AcAccount, user: AcUser) -> AcToken:
|
||||||
|
authenticator = AcTokenAuthenticator(account.url + '/api/v%s' % AC_API_VERSION)
|
||||||
|
res = authenticator.issue_token_intent(user.intent)
|
||||||
|
if res.status_code != 200:
|
||||||
|
raise Exception('Request token failed!')
|
||||||
|
res_data = res.json()
|
||||||
|
if res_data['is_ok'] != 1:
|
||||||
|
raise Exception('Request token failed! (2)')
|
||||||
|
return AcToken(res_data["token"])
|
||||||
|
|
||||||
|
def get_info(self):
|
||||||
|
client = AcClient(self.session.cur_account, self.session.token)
|
||||||
|
res = client.get_info()
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def get_projects(self) -> list[AcProject]:
|
||||||
|
client = AcClient(self.session.cur_account, self.session.token)
|
||||||
|
res = client.get_projects()
|
||||||
|
if res.status_code != 200:
|
||||||
|
raise Exception("Error %d" % res.status_code)
|
||||||
|
res_data = res.json()
|
||||||
|
projects = list(map(lambda p: project_from_json(p), res_data))
|
||||||
|
return projects
|
||||||
|
|
||||||
|
def get_subtasks(self, project_id: int, task_id: int) -> list[AcSubtask]:
|
||||||
|
client = AcClient(self.session.cur_account, self.session.token)
|
||||||
|
res = client.get_project_task(project_id, task_id)
|
||||||
|
res_data = res.json()
|
||||||
|
subtasks = list(map(lambda sub: subtask_from_json(sub), res_data["subtasks"]))
|
||||||
|
return subtasks
|
||||||
|
|
||||||
|
def create_subtask(self, subtask: AcSubtask):
|
||||||
|
client = AcClient(self.session.cur_account, self.session.token)
|
||||||
|
res = client.create_subtask(subtask)
|
||||||
|
res_data = res.json()
|
||||||
|
return res_data
|
||||||
+1
-1
@@ -5,7 +5,7 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|||||||
RUN apt-get clean \
|
RUN apt-get clean \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get upgrade -y
|
&& apt-get upgrade -y
|
||||||
RUN apt-get install -y python3 python3-pip
|
RUN apt-get install -y python3 python3-pip build-essential
|
||||||
COPY requirements.txt /tmp/requirements.txt
|
COPY requirements.txt /tmp/requirements.txt
|
||||||
RUN pip3 install -r /tmp/requirements.txt
|
RUN pip3 install -r /tmp/requirements.txt
|
||||||
|
|
||||||
|
|||||||
@@ -14,39 +14,37 @@
|
|||||||
2. Request a token for some Account (user could have multiple accounts) using the Account `url` and the users `intent`
|
2. Request a token for some Account (user could have multiple accounts) using the Account `url` and the users `intent`
|
||||||
3. Use the token to authenticate for every other API-Call
|
3. Use the token to authenticate for every other API-Call
|
||||||
|
|
||||||
Example login:
|
## CLI (Work in progress)
|
||||||
|
|
||||||
```python
|
> activecollab [options] [object] [command] {arguments}
|
||||||
auth = ACAuthenticator()
|
|
||||||
session = auth.login(username, password)
|
|
||||||
pprint(session)
|
|
||||||
user = session['user']
|
|
||||||
# use first account
|
|
||||||
account = auth.find_account(account_name)
|
|
||||||
```
|
|
||||||
|
|
||||||
Example request token
|
The output will be always serialized to JSON.
|
||||||
|
|
||||||
```python
|
|
||||||
token = ACTokenAuthenticator(account, user).issue_token_intent()
|
|
||||||
pprint(token)
|
|
||||||
```
|
|
||||||
|
|
||||||
Example client request
|
### Options
|
||||||
|
|
||||||
```python
|
Mandatory options:
|
||||||
ac = ACClient(account, token)
|
- config=<file> - read config from file (json)
|
||||||
res = ac.get_projects()
|
|
||||||
pprint(res.json())
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
### Object
|
||||||
|
|
||||||
config.ini:
|
- info - print version nummer from server
|
||||||
|
- account - all accounts you have access to with your login
|
||||||
|
- project - manage projects in Active Collab
|
||||||
|
|
||||||
|
Future:
|
||||||
|
- task
|
||||||
|
- subtask
|
||||||
|
- people
|
||||||
|
- invoice
|
||||||
|
|
||||||
|
### Command
|
||||||
|
|
||||||
|
- list - List all items
|
||||||
|
|
||||||
|
Future:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- delete
|
||||||
|
|
||||||
```ini
|
|
||||||
[LOGIN]
|
|
||||||
username = you@example.com
|
|
||||||
password = very-secret
|
|
||||||
account_name = "#123456"
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
## Backlog
|
||||||
|
|
||||||
|
* Tests for CLI
|
||||||
|
* pack with https://python-poetry.org
|
||||||
|
* publish first version on github
|
||||||
|
* complete [API](https://developers.activecollab.com/api-documentation/index.html)
|
||||||
|
* Project list, get, create, delete
|
||||||
|
* Task list, get, create, delete
|
||||||
|
* Tasklist
|
||||||
|
* Subtask list, get, create, delete
|
||||||
|
* Attachments
|
||||||
|
* Companies
|
||||||
|
* Users
|
||||||
|
* Teams
|
||||||
|
* Notes
|
||||||
|
* support for paging
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
while IFS= read -r TEXT
|
||||||
|
do
|
||||||
|
python3 main.py -c ./config-mira.json subtask create --project 310 --task 6253 --body "$TEXT"
|
||||||
|
done
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import json
|
||||||
|
from pprint import pprint
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from ActiveCollabAPI.ActiveCollab import ActiveCollab
|
||||||
|
|
||||||
|
with open("test-config.json", "r") as cfg:
|
||||||
|
config = json.load(cfg)
|
||||||
|
|
||||||
|
|
||||||
|
class Test_Integration_Login(TestCase):
|
||||||
|
|
||||||
|
def test_login_success(self):
|
||||||
|
ac = ActiveCollab(config["base_url"])
|
||||||
|
session = ac.login_to_first_account(config["username"], config["password"])
|
||||||
|
self.assertEqual(config["first_name"], session.user.first_name)
|
||||||
|
self.assertEqual(config["last_name"], session.user.last_name)
|
||||||
|
self.assertEqual(1, len(session.accounts))
|
||||||
|
self.assertGreater(len(session.token.token), 5)
|
||||||
|
|
||||||
|
def test_get_projects(self):
|
||||||
|
ac = ActiveCollab(config["base_url"])
|
||||||
|
ac.login_to_first_account(config["username"], config["password"])
|
||||||
|
projects = ac.get_projects()
|
||||||
|
self.assertGreater(len(projects), 0)
|
||||||
|
self.assertEqual('Project', projects[0].class_)
|
||||||
|
self.assertGreater(projects[0].id, 0)
|
||||||
@@ -1,102 +1,110 @@
|
|||||||
import configparser
|
import argparse
|
||||||
import logging
|
import json
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
from ActiveCollabAPI.ACAuthenticator import ACAuthenticator
|
from ActiveCollabAPI.AcSubtask import AcSubtask
|
||||||
from ActiveCollabAPI.ACClient import ACClient
|
from ActiveCollabAPI.ActiveCollab import ActiveCollab
|
||||||
from ActiveCollabAPI.ACTokenAuthenticator import ACTokenAuthenticator
|
|
||||||
|
|
||||||
# Enabling debugging at http.client level (requests->urllib3->http.client)
|
|
||||||
# you will see the REQUEST, including HEADERS and DATA, and RESPONSE with HEADERS but without DATA.
|
|
||||||
# the only thing missing will be the response.body which is not logged.
|
|
||||||
from http.client import HTTPConnection
|
|
||||||
|
|
||||||
HTTPConnection.debuglevel = 1
|
def run(args, config):
|
||||||
|
# run the command
|
||||||
|
ac = ActiveCollab(config["base_url"])
|
||||||
|
ac.login_to_first_account(config["username"], config["password"])
|
||||||
|
output = None
|
||||||
|
if args.object == "info":
|
||||||
|
output = run_info(ac, args)
|
||||||
|
if args.object == "account":
|
||||||
|
output = run_account(ac, args)
|
||||||
|
if args.object == "project":
|
||||||
|
output = run_project(ac, args, output)
|
||||||
|
if args.object == "subtask":
|
||||||
|
output = run_subtask(ac, args)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def run_account(ac, args):
|
||||||
|
if args.command == "list":
|
||||||
|
return ac.session.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
def run_project(ac, args, output):
|
||||||
|
if args.command == "list":
|
||||||
|
output = list(map(lambda p: p.to_dict(), ac.get_projects()))
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def run_subtask(ac, args):
|
||||||
|
pprint(args)
|
||||||
|
output = None
|
||||||
|
if args.command == "list":
|
||||||
|
output = list(map(lambda sub: sub.to_dict(), ac.get_subtasks(args.project, args.task)))
|
||||||
|
if args.command == "create":
|
||||||
|
subtask = AcSubtask(
|
||||||
|
project_id=args.project,
|
||||||
|
task_id=args.task,
|
||||||
|
body=args.body
|
||||||
|
)
|
||||||
|
output = ac.create_subtask(subtask)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def run_info(ac, args):
|
||||||
|
output = ac.get_info()
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_output(output):
|
||||||
|
# serialize the output
|
||||||
|
return json.dumps(output)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(args):
|
||||||
|
with open(args.config, "r", encoding="utf8") as fh:
|
||||||
|
config = json.load(fh)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# parse arguments
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='activecollab',
|
||||||
|
description='This is a CLI Client to Active-Collab Service',
|
||||||
|
epilog='This is not a offical tool provided by the AC Guys!')
|
||||||
|
parser.add_argument('-c', '--config', required=True,
|
||||||
|
help="use the named config file")
|
||||||
|
parser.add_argument('-v', '--version', action='store_true',
|
||||||
|
help="show version information for this tool")
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(help='sub-command help')
|
||||||
|
|
||||||
|
# info list
|
||||||
|
parser_account = subparsers.add_parser('info', help='info help')
|
||||||
|
parser_account.set_defaults(object='info')
|
||||||
|
|
||||||
|
# account list
|
||||||
|
parser_account = subparsers.add_parser('account', help='account help')
|
||||||
|
parser_account.set_defaults(object='account')
|
||||||
|
parser_account.add_argument('command', choices=["list"], help='list all accessible accounts')
|
||||||
|
|
||||||
|
# project list get
|
||||||
|
parser_project = subparsers.add_parser('project', help='project help')
|
||||||
|
parser_project.set_defaults(object='project')
|
||||||
|
parser_project.add_argument('command', choices=["list"], help='what to do with the account help')
|
||||||
|
|
||||||
|
# subtask
|
||||||
|
parser_subtask = subparsers.add_parser('subtask', help='subtask help')
|
||||||
|
parser_subtask.set_defaults(object='subtask')
|
||||||
|
parser_subtask.add_argument('command', choices=["list", "create"], help='what to do with the account help')
|
||||||
|
parser_subtask.add_argument('--project', type=int, required=True)
|
||||||
|
parser_subtask.add_argument('--task', type=int, required=True)
|
||||||
|
parser_subtask.add_argument('--body', type=str)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
config = load_config(args)
|
||||||
|
output = run(args, config)
|
||||||
|
print(serialize_output(output))
|
||||||
|
|
||||||
logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from requests
|
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
|
||||||
requests_log = logging.getLogger("urllib3")
|
|
||||||
requests_log.setLevel(logging.DEBUG)
|
|
||||||
requests_log.propagate = True
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
config = configparser.ConfigParser()
|
main()
|
||||||
config.read('config.ini')
|
|
||||||
|
|
||||||
username = config['LOGIN']['username']
|
|
||||||
password = config['LOGIN']['password']
|
|
||||||
account_name = config['LOGIN']['account_name']
|
|
||||||
|
|
||||||
# 1. you need to log in to get a session and a list of accounts
|
|
||||||
auth = ACAuthenticator()
|
|
||||||
session = auth.login(username, password)
|
|
||||||
pprint(session)
|
|
||||||
user = session['user']
|
|
||||||
# use first account
|
|
||||||
account = auth.find_account(account_name)
|
|
||||||
|
|
||||||
# 2. you need to get a token for this account
|
|
||||||
# token = ACTokenAuthenticator(account, user).issue_token_intent()
|
|
||||||
# pprint(token)
|
|
||||||
|
|
||||||
# 3. now you can query the API
|
|
||||||
# ac = ACClient(account, token)
|
|
||||||
|
|
||||||
# get a list of all projects
|
|
||||||
# res = ac.get_projects()
|
|
||||||
# pprint(res.json())
|
|
||||||
|
|
||||||
# get task with sub tasks
|
|
||||||
# https://app.activecollab.com/416910/projects/310/tasks/6274
|
|
||||||
# project_id = 310
|
|
||||||
# task_id = 6281
|
|
||||||
# task_res = ac.get_project_task(project_id, task_id)
|
|
||||||
# pprint(task_res.json())
|
|
||||||
# task = task_res.json()
|
|
||||||
# res = ac.unassign_all_sub_task(task)
|
|
||||||
|
|
||||||
# create a new subtask for the task without assignment
|
|
||||||
# subtask = {
|
|
||||||
# "task_id": task_id,
|
|
||||||
# "project_id": project_id,
|
|
||||||
# "assignee_id": "",
|
|
||||||
# "body": "test qqq"
|
|
||||||
# }
|
|
||||||
# res = ac.create_subtask(subtask)
|
|
||||||
# pprint(res.json())
|
|
||||||
|
|
||||||
# # bulk create subtasks
|
|
||||||
subtask_texts = []
|
|
||||||
with open("sub-tasks.txt", "r", encoding="utf-8") as fh:
|
|
||||||
for line in fh:
|
|
||||||
subtask_texts.append(line.rstrip('\n'))
|
|
||||||
# create
|
|
||||||
# for text in subtask_texts:
|
|
||||||
# subtask['body'] = text
|
|
||||||
# ac.create_subtask(subtask)
|
|
||||||
# delete
|
|
||||||
# for text in subtask_texts:
|
|
||||||
# subtask = ac.find_subtask_with_body(task, text)
|
|
||||||
# if subtask is not None:
|
|
||||||
# print(subtask)
|
|
||||||
# ac.delete_subtask(subtask)
|
|
||||||
|
|
||||||
# # unassign a single subtask
|
|
||||||
# subtask_id = 12203
|
|
||||||
# subtask_res = ac.get_project_task_subtask(project_id, task_id, subtask_id)
|
|
||||||
# # pprint(subtask_res.json())
|
|
||||||
# subtask = subtask_res.json()['single']
|
|
||||||
# res = ac.unassign_sub_task(subtask)
|
|
||||||
# pprint(res.json())
|
|
||||||
|
|
||||||
# delete a subtask
|
|
||||||
# subtask = {
|
|
||||||
# "task_id": task_id,
|
|
||||||
# "project_id": project_id,
|
|
||||||
# "id": 13764
|
|
||||||
# }
|
|
||||||
# res = ac.delete_subtask(subtask)
|
|
||||||
# pprint(res.json())
|
|
||||||
|
|
||||||
# res = ac.get_info()
|
|
||||||
# pprint(res.json())
|
|
||||||
|
|||||||
+55
-36
@@ -1,36 +1,55 @@
|
|||||||
S3 Bucket "320dev-public" is archived
|
PDF Export for Space '320EAST Human Resources (HR)' done
|
||||||
S3 Bucket "320east-acm2-archive" is archived
|
PDF Export for Space '320EAST Legal' done
|
||||||
S3 Bucket "assets-prodeuall1-320east" is archived
|
PDF Export for Space '320EAST Management (MGT)' done
|
||||||
S3 Bucket "assets-sdds-01-320east" is archived
|
PDF Export for Space '320EAST Management (MGT) Leaders' done
|
||||||
S3 Bucket "backups-mattermost" is archived
|
PDF Export for Space '320EAST Marketing (MKTG)' done
|
||||||
S3 Bucket "bucket.noc.320east.io" is archived
|
PDF Export for Space '320EAST Office Management (OM)' done
|
||||||
S3 Bucket "lcmx-master-320east" is archived
|
PDF Export for Space '320EAST Partner Management (PARMGT)' done
|
||||||
S3 Bucket "lcmx-prodeuall1-320east" is archived
|
PDF Export for Space '320EAST Procurement & Supply (PROSU)' done
|
||||||
S3 Bucket "shop320-backups" is archived
|
PDF Export for Space '320EAST Program Management (PROGMGT)' done
|
||||||
S3 Bucket "320dev-public" is deleted
|
PDF Export for Space '320EAST PROSU Risk Management (PROR)' done
|
||||||
S3 Bucket "320east-acm2-archive" is deleted
|
PDF Export for Space '320EAST Research & Development (TELA)' done
|
||||||
S3 Bucket "assets-prodeuall1-320east" is deleted
|
PDF Export for Space '320EAST Sales & Business Development (HOKOCOKO)' done
|
||||||
S3 Bucket "assets-sdds-01-320east" is deleted
|
PDF Export for Space 'ak.320east.de' done
|
||||||
S3 Bucket "backups-mattermost" is deleted
|
PDF Export for Space 'Antonio Sciara' done
|
||||||
S3 Bucket "backups-prodeuall1-320east" is deleted
|
PDF Export for Space 'Atlassian 320EAST' done
|
||||||
S3 Bucket "bigdataioav-idev0052-prodeuall1-320east" is deleted
|
PDF Export for Space 'calvin.chen.ngstb.com' done
|
||||||
S3 Bucket "bucket.noc.320east.io" is deleted
|
PDF Export for Space 'catherine.tang.tv2next.com' done
|
||||||
S3 Bucket "lcmx-master-320east" is deleted
|
PDF Export for Space 'cs.320east.de' done
|
||||||
S3 Bucket "lcmx-prodeuall1-320east" is deleted
|
PDF Export for Space 'dp.320east.de' done
|
||||||
S3 Bucket "shop320-backups" is deleted
|
PDF Export for Space 'Emilia Biche' done
|
||||||
Cloudfront Distribution "E7R0HGAKU10KT - Production d1w1tdkpk45r2q.cloudfront.net sdds.homeok.io assets-sdds-01-320east.s3.eu-west-1.amazonaws.com" is removed
|
PDF Export for Space 'ewe.320east.de' done
|
||||||
Cloudfront Distribution "E1YAR9WILYEAOR - Production d283m0l3zuk2cl.cloudfront.net bucket.noc.320east.io.s3.eu-west-1.amazonaws.com" is removed
|
PDF Export for Space 'hansen.he.tv2next.com' done
|
||||||
Cloudfront Distribution "E2HUPIVJFDUSWF - Production dvfi2qvbg5wxw.cloudfront.net www.pds.companyok.io, pds.companyok.io assets-pds-companyok.s3.amazonaws.com" is removed
|
PDF Export for Space 'hap.320east.de' done
|
||||||
Cloudfront Distribution "E2Q6TWJX3L3TRS - Production d3gvb7cyihtwwp.cloudfront.net s3-h38-lcm.320east.net.s3.amazonaws.com" is removed
|
PDF Export for Space 'hs.320east.de' done
|
||||||
Cloudfront Distribution "E215TJ62TWJBBU - Production d2aseehijcy5it.cloudfront.net assets-sdds.s3.amazonaws.com" is removed
|
PDF Export for Space 'jl.320east.de' done
|
||||||
IAM User "320east-ansible" is removed
|
PDF Export for Space 'jle.320east.de' done
|
||||||
IAM User "backup" is removed
|
PDF Export for Space 'Kaumudi Deshmukh' done
|
||||||
IAM User "cs" is removed
|
PDF Export for Space 'ks.320east.de' done
|
||||||
IAM User "docker-registry" is removed
|
PDF Export for Space 'll.320east.de' done
|
||||||
IAM User "io-bigdataioav-prod320" is removed
|
PDF Export for Space 'Myrthe Günther' done
|
||||||
IAM User "lcm-h38-prod Active" is removed
|
PDF Export for Space 'Pooja Naringrekar' done
|
||||||
IAM User "mattermost-backup" is removed
|
PDF Export for Space 'rain.chen.tv2next.com' done
|
||||||
IAM Usergroup "admin" is removed
|
PDF Export for Space 'Robert Uhlmann' done
|
||||||
IAM Usergroup "ansible" is removed
|
PDF Export for Space 'sa.320east.de' done
|
||||||
IAM Usergroup "backup" is removed
|
PDF Export for Space 'silke.heim.awpartner.de' done
|
||||||
IAM Usergroup "lcm" is removed
|
PDF Export for Space 'tgr.320east.de' done
|
||||||
|
PDF Export for Space 'thomas.link.awpartner.de' done
|
||||||
|
PDF Export for Space 'thomas.wei.ngstb.com' done
|
||||||
|
PDF Export for Space 'tig.320east.de' done
|
||||||
|
PDF Export for Space 'tiz.320east.de' done
|
||||||
|
PDF Export for Space 'TNC320 Readme' done
|
||||||
|
PDF Export for Space 'TNC Accounting (AFI)' done
|
||||||
|
PDF Export for Space 'TNC Board' done
|
||||||
|
PDF Export for Space 'TNC Business Unit sprint:bricks' done
|
||||||
|
PDF Export for Space 'TNC Human Resources (HR)' done
|
||||||
|
PDF Export for Space 'TNC Marketing (MKTG)' done
|
||||||
|
PDF Export for Space 'TNC Office Management (OM)' done
|
||||||
|
PDF Export for Space 'TNC Product Development (PD)' done
|
||||||
|
PDF Export for Space 'TNC Public' done
|
||||||
|
PDF Export for Space 'TNC Purchase' done
|
||||||
|
PDF Export for Space 'TNC Quality Assurance (QA)' done
|
||||||
|
PDF Export for Space 'TNC Sales' done
|
||||||
|
PDF Export for Space 'ts.320east.de' done
|
||||||
|
PDF Export for Space 'zh.320east.de' done
|
||||||
|
PDF Export for Space 'zhengyi.tv2next.com' done
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"base_url": "https://activecollab.com/api/v1/external/login",
|
||||||
|
"username": "packend_ja.05@icloud.com",
|
||||||
|
"password": "myqkyc-muntuh-haRti6",
|
||||||
|
|
||||||
|
"first_name": "packend_ja",
|
||||||
|
"last_name": ".05@icloud.com"
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import json
|
||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from ActiveCollabAPI import AC_LOGIN_BASE_URL
|
||||||
|
from ActiveCollabAPI.AcAuthenticator import AcAuthenticator
|
||||||
|
|
||||||
|
|
||||||
|
class TestAcAuthenticator(TestCase):
|
||||||
|
|
||||||
|
def mock_login_success(self):
|
||||||
|
return {
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": 12345678,
|
||||||
|
"url": "https:\/\/app.activecollab.com\/12345678",
|
||||||
|
"display_name": "433339",
|
||||||
|
"user_display_name": "#12345678",
|
||||||
|
"position": 1,
|
||||||
|
"class": "ActiveCollab\\Shepherd\\Model\\Account\\ActiveCollab\\FeatherAccount",
|
||||||
|
"status": "trial"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_ok": 1,
|
||||||
|
"user": {
|
||||||
|
"avatar_url": "https:\/\/activecollab.com\/avatar.php?user_id=12345678&size=80",
|
||||||
|
"first_name": "users first",
|
||||||
|
"last_name": "users last",
|
||||||
|
"intent": "random-string"}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_success_gets_user_and_accounts_list(self):
|
||||||
|
with patch("ActiveCollabAPI.AcAuthenticator.requests.post") as mock_post:
|
||||||
|
mock_post.return_value.status_code = 200
|
||||||
|
mock_post.return_value.json.return_value = self.mock_login_success()
|
||||||
|
|
||||||
|
email = "collab@example.com"
|
||||||
|
password = "VeryS3cret!"
|
||||||
|
authenticator = AcAuthenticator(AC_LOGIN_BASE_URL)
|
||||||
|
response = authenticator.login(email, password)
|
||||||
|
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
res_data = response.json()
|
||||||
|
self.assertEqual(1, res_data['is_ok'])
|
||||||
|
user = res_data["user"]
|
||||||
|
self.assertEqual("users first", user["first_name"])
|
||||||
|
self.assertEqual("users last", user["last_name"])
|
||||||
|
accounts = res_data["accounts"]
|
||||||
|
self.assertGreater(len(accounts), 0)
|
||||||
|
self.assertEqual(12345678, accounts[0]["name"])
|
||||||
|
|
||||||
|
|
||||||
|
def mock_login_failed(self):
|
||||||
|
return {
|
||||||
|
"is_ok": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_login_failed_not_ok(self):
|
||||||
|
with patch("ActiveCollabAPI.AcAuthenticator.requests.post") as mock_post:
|
||||||
|
mock_post.return_value.status_code = 200
|
||||||
|
mock_post.return_value.json.return_value = self.mock_login_failed()
|
||||||
|
|
||||||
|
email = "collab@example.com"
|
||||||
|
password = "VeryS3cret!"
|
||||||
|
authenticator = AcAuthenticator(AC_LOGIN_BASE_URL)
|
||||||
|
response = authenticator.login(email, password)
|
||||||
|
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
res_data = response.json()
|
||||||
|
self.assertNotEqual(1, res_data['is_ok'])
|
||||||
|
|
||||||
|
|
||||||
|
def mock_login_forbidden(self):
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
def test_login_forbidden(self):
|
||||||
|
with patch("ActiveCollabAPI.AcAuthenticator.requests.post") as mock_post:
|
||||||
|
mock_post.return_value.status_code = 401
|
||||||
|
mock_post.return_value.json.return_value = self.mock_login_forbidden()
|
||||||
|
|
||||||
|
email = "collab@example.com"
|
||||||
|
password = "VeryS3cret!"
|
||||||
|
authenticator = AcAuthenticator(AC_LOGIN_BASE_URL)
|
||||||
|
response = authenticator.login(email, password)
|
||||||
|
|
||||||
|
self.assertEqual(401, response.status_code)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from ActiveCollabAPI.AcTokenAuthenticator import AcTokenAuthenticator
|
||||||
|
|
||||||
|
|
||||||
|
class TestAcTokenAuthenticator(TestCase):
|
||||||
|
|
||||||
|
def mock_token_success(self):
|
||||||
|
return {
|
||||||
|
"is_ok": 1,
|
||||||
|
"token": "abcdef"
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_issue_token_intent_success(self):
|
||||||
|
with patch("ActiveCollabAPI.AcTokenAuthenticator.requests.post") as mock_post:
|
||||||
|
mock_post.return_value.status_code = 200
|
||||||
|
mock_post.return_value.json.return_value = self.mock_token_success()
|
||||||
|
|
||||||
|
auth = AcTokenAuthenticator("http://mock")
|
||||||
|
res = auth.issue_token_intent("this-is-the-token-intent-string")
|
||||||
|
|
||||||
|
self.assertEqual(200, res.status_code)
|
||||||
|
res_data = res.json()
|
||||||
|
self.assertEqual(1, res_data["is_ok"])
|
||||||
|
|
||||||
|
def mock_token_not_ok(self):
|
||||||
|
return {
|
||||||
|
"is_ok": 0,
|
||||||
|
"token": "abcdef"
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_issue_token_intent_not_ok(self):
|
||||||
|
with patch("ActiveCollabAPI.AcTokenAuthenticator.requests.post") as mock_post:
|
||||||
|
mock_post.return_value.status_code = 200
|
||||||
|
mock_post.return_value.json.return_value = self.mock_token_not_ok()
|
||||||
|
|
||||||
|
auth = AcTokenAuthenticator("http://mock")
|
||||||
|
res = auth.issue_token_intent("this-is-the-token-intent-string")
|
||||||
|
|
||||||
|
self.assertEqual(200, res.status_code)
|
||||||
|
res_data = res.json()
|
||||||
|
self.assertNotEqual(1, res_data["is_ok"])
|
||||||
|
|
||||||
|
def mock_token_forbidden(self):
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_issue_token_intent_forbidden(self):
|
||||||
|
with patch("ActiveCollabAPI.AcTokenAuthenticator.requests.post") as mock_post:
|
||||||
|
mock_post.return_value.status_code = 401
|
||||||
|
mock_post.return_value.json.return_value = self.mock_token_forbidden()
|
||||||
|
|
||||||
|
auth = AcTokenAuthenticator("http://mock")
|
||||||
|
res = auth.issue_token_intent("this-is-the-token-intent-string")
|
||||||
|
|
||||||
|
self.assertEqual(401, res.status_code)
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user