Merge branch 'develop'
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
config.ini
|
||||
.venv/
|
||||
__pycache__/
|
||||
/config-mira.ini
|
||||
/config-mira.json
|
||||
|
||||
Generated
+2
@@ -3,6 +3,8 @@
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<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" />
|
||||
</content>
|
||||
<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
|
||||
|
||||
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
|
||||
account = None
|
||||
token = None
|
||||
|
||||
def __init__(self, account, token):
|
||||
def __init__(self, account: AcAccount, token: AcToken):
|
||||
self.account = account
|
||||
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):
|
||||
return {
|
||||
'Content-Type': 'application/json; charset=utf8',
|
||||
'Accept': 'application/json',
|
||||
'X-Angie-AuthApiToken': self.token,
|
||||
'X-Angie-AuthApiToken': self.token.token,
|
||||
'User-Agent': AC_USER_AGENT
|
||||
}
|
||||
|
||||
@@ -90,12 +96,12 @@ class ACClient:
|
||||
return self._delete('projects/%d/tasks/%s/subtasks/%s' %
|
||||
(project_id, task_id, subtask_id))
|
||||
|
||||
def create_subtask(self, subtask):
|
||||
project_id = subtask['project_id']
|
||||
task_id = subtask['task_id']
|
||||
def create_subtask(self, subtask: AcSubtask):
|
||||
project_id = subtask.project_id
|
||||
task_id = subtask.task_id
|
||||
return self._post('projects/%d/tasks/%s/subtasks' %
|
||||
(project_id, task_id),
|
||||
data=subtask)
|
||||
data=subtask.to_dict())
|
||||
|
||||
# find subtask with matching body
|
||||
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 \
|
||||
&& apt-get update \
|
||||
&& 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
|
||||
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`
|
||||
3. Use the token to authenticate for every other API-Call
|
||||
|
||||
Example login:
|
||||
## CLI (Work in progress)
|
||||
|
||||
```python
|
||||
auth = ACAuthenticator()
|
||||
session = auth.login(username, password)
|
||||
pprint(session)
|
||||
user = session['user']
|
||||
# use first account
|
||||
account = auth.find_account(account_name)
|
||||
```
|
||||
> activecollab [options] [object] [command] {arguments}
|
||||
|
||||
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
|
||||
ac = ACClient(account, token)
|
||||
res = ac.get_projects()
|
||||
pprint(res.json())
|
||||
```
|
||||
Mandatory options:
|
||||
- config=<file> - read config from file (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 logging
|
||||
import argparse
|
||||
import json
|
||||
from pprint import pprint
|
||||
|
||||
from ActiveCollabAPI.ACAuthenticator import ACAuthenticator
|
||||
from ActiveCollabAPI.ACClient import ACClient
|
||||
from ActiveCollabAPI.ACTokenAuthenticator import ACTokenAuthenticator
|
||||
from ActiveCollabAPI.AcSubtask import AcSubtask
|
||||
from ActiveCollabAPI.ActiveCollab import ActiveCollab
|
||||
|
||||
# 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__":
|
||||
config = configparser.ConfigParser()
|
||||
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())
|
||||
main()
|
||||
|
||||
+55
-36
@@ -1,36 +1,55 @@
|
||||
S3 Bucket "320dev-public" is archived
|
||||
S3 Bucket "320east-acm2-archive" is archived
|
||||
S3 Bucket "assets-prodeuall1-320east" is archived
|
||||
S3 Bucket "assets-sdds-01-320east" is archived
|
||||
S3 Bucket "backups-mattermost" is archived
|
||||
S3 Bucket "bucket.noc.320east.io" is archived
|
||||
S3 Bucket "lcmx-master-320east" is archived
|
||||
S3 Bucket "lcmx-prodeuall1-320east" is archived
|
||||
S3 Bucket "shop320-backups" is archived
|
||||
S3 Bucket "320dev-public" is deleted
|
||||
S3 Bucket "320east-acm2-archive" is deleted
|
||||
S3 Bucket "assets-prodeuall1-320east" is deleted
|
||||
S3 Bucket "assets-sdds-01-320east" is deleted
|
||||
S3 Bucket "backups-mattermost" is deleted
|
||||
S3 Bucket "backups-prodeuall1-320east" is deleted
|
||||
S3 Bucket "bigdataioav-idev0052-prodeuall1-320east" is deleted
|
||||
S3 Bucket "bucket.noc.320east.io" is deleted
|
||||
S3 Bucket "lcmx-master-320east" is deleted
|
||||
S3 Bucket "lcmx-prodeuall1-320east" is deleted
|
||||
S3 Bucket "shop320-backups" is deleted
|
||||
Cloudfront Distribution "E7R0HGAKU10KT - Production d1w1tdkpk45r2q.cloudfront.net sdds.homeok.io assets-sdds-01-320east.s3.eu-west-1.amazonaws.com" is removed
|
||||
Cloudfront Distribution "E1YAR9WILYEAOR - Production d283m0l3zuk2cl.cloudfront.net bucket.noc.320east.io.s3.eu-west-1.amazonaws.com" is removed
|
||||
Cloudfront Distribution "E2HUPIVJFDUSWF - Production dvfi2qvbg5wxw.cloudfront.net www.pds.companyok.io, pds.companyok.io assets-pds-companyok.s3.amazonaws.com" is removed
|
||||
Cloudfront Distribution "E2Q6TWJX3L3TRS - Production d3gvb7cyihtwwp.cloudfront.net s3-h38-lcm.320east.net.s3.amazonaws.com" is removed
|
||||
Cloudfront Distribution "E215TJ62TWJBBU - Production d2aseehijcy5it.cloudfront.net assets-sdds.s3.amazonaws.com" is removed
|
||||
IAM User "320east-ansible" is removed
|
||||
IAM User "backup" is removed
|
||||
IAM User "cs" is removed
|
||||
IAM User "docker-registry" is removed
|
||||
IAM User "io-bigdataioav-prod320" is removed
|
||||
IAM User "lcm-h38-prod Active" is removed
|
||||
IAM User "mattermost-backup" is removed
|
||||
IAM Usergroup "admin" is removed
|
||||
IAM Usergroup "ansible" is removed
|
||||
IAM Usergroup "backup" is removed
|
||||
IAM Usergroup "lcm" is removed
|
||||
PDF Export for Space '320EAST Human Resources (HR)' done
|
||||
PDF Export for Space '320EAST Legal' done
|
||||
PDF Export for Space '320EAST Management (MGT)' done
|
||||
PDF Export for Space '320EAST Management (MGT) Leaders' done
|
||||
PDF Export for Space '320EAST Marketing (MKTG)' done
|
||||
PDF Export for Space '320EAST Office Management (OM)' done
|
||||
PDF Export for Space '320EAST Partner Management (PARMGT)' done
|
||||
PDF Export for Space '320EAST Procurement & Supply (PROSU)' done
|
||||
PDF Export for Space '320EAST Program Management (PROGMGT)' done
|
||||
PDF Export for Space '320EAST PROSU Risk Management (PROR)' done
|
||||
PDF Export for Space '320EAST Research & Development (TELA)' done
|
||||
PDF Export for Space '320EAST Sales & Business Development (HOKOCOKO)' done
|
||||
PDF Export for Space 'ak.320east.de' done
|
||||
PDF Export for Space 'Antonio Sciara' done
|
||||
PDF Export for Space 'Atlassian 320EAST' done
|
||||
PDF Export for Space 'calvin.chen.ngstb.com' done
|
||||
PDF Export for Space 'catherine.tang.tv2next.com' done
|
||||
PDF Export for Space 'cs.320east.de' done
|
||||
PDF Export for Space 'dp.320east.de' done
|
||||
PDF Export for Space 'Emilia Biche' done
|
||||
PDF Export for Space 'ewe.320east.de' done
|
||||
PDF Export for Space 'hansen.he.tv2next.com' done
|
||||
PDF Export for Space 'hap.320east.de' done
|
||||
PDF Export for Space 'hs.320east.de' done
|
||||
PDF Export for Space 'jl.320east.de' done
|
||||
PDF Export for Space 'jle.320east.de' done
|
||||
PDF Export for Space 'Kaumudi Deshmukh' done
|
||||
PDF Export for Space 'ks.320east.de' done
|
||||
PDF Export for Space 'll.320east.de' done
|
||||
PDF Export for Space 'Myrthe Günther' done
|
||||
PDF Export for Space 'Pooja Naringrekar' done
|
||||
PDF Export for Space 'rain.chen.tv2next.com' done
|
||||
PDF Export for Space 'Robert Uhlmann' done
|
||||
PDF Export for Space 'sa.320east.de' done
|
||||
PDF Export for Space 'silke.heim.awpartner.de' done
|
||||
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