Merge branch 'develop'

This commit is contained in:
2024-03-01 09:47:36 +01:00
29 changed files with 793 additions and 259 deletions
+2
View File
@@ -1,3 +1,5 @@
config.ini config.ini
.venv/ .venv/
__pycache__/ __pycache__/
/config-mira.ini
/config-mira.json
+2
View File
@@ -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
View File
@@ -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="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;discover integration_tests&quot;" />
<option name="_new_target" value="&quot;&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>
</component>
+20
View File
@@ -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="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;discover tests&quot;" />
<option name="_new_target" value="&quot;&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>
</component>
-48
View File
@@ -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]
-43
View File
@@ -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
}
+28
View File
@@ -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)
+32
View File
@@ -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):
+10
View File
@@ -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]
+65
View File
@@ -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)
+23
View File
@@ -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())
+46
View File
@@ -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)
+6
View File
@@ -0,0 +1,6 @@
from dataclasses import dataclass
@dataclass
class AcToken:
token: str
+33
View File
@@ -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))
+18
View File
@@ -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())
+82
View File
@@ -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
View File
@@ -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
+26 -28
View File
@@ -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"
```
+17
View File
@@ -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
+7
View File
@@ -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
+2
View File
@@ -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)
+103 -95
View File
@@ -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
View File
@@ -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
+8
View File
@@ -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"
}
View File
+87
View File
@@ -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)
+59
View File
@@ -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)