diff --git a/.gitignore b/.gitignore index d708872..4012092 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ config.ini .venv/ __pycache__/ +/config-mira.ini +/config-mira.json diff --git a/.idea/ActiveCollabAPI.iml b/.idea/ActiveCollabAPI.iml index 078a7e4..b52f417 100644 --- a/.idea/ActiveCollabAPI.iml +++ b/.idea/ActiveCollabAPI.iml @@ -3,6 +3,8 @@ + + diff --git a/.idea/runConfigurations/Integrationtests.xml b/.idea/runConfigurations/Integrationtests.xml new file mode 100644 index 0000000..675ce8b --- /dev/null +++ b/.idea/runConfigurations/Integrationtests.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Unittests.xml b/.idea/runConfigurations/Unittests.xml new file mode 100644 index 0000000..0a85783 --- /dev/null +++ b/.idea/runConfigurations/Unittests.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/ActiveCollabAPI/ACAuthenticator.py b/ActiveCollabAPI/ACAuthenticator.py deleted file mode 100644 index 0c96fb6..0000000 --- a/ActiveCollabAPI/ACAuthenticator.py +++ /dev/null @@ -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] diff --git a/ActiveCollabAPI/ACTokenAuthenticator.py b/ActiveCollabAPI/ACTokenAuthenticator.py deleted file mode 100644 index ba1f929..0000000 --- a/ActiveCollabAPI/ACTokenAuthenticator.py +++ /dev/null @@ -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 - } diff --git a/ActiveCollabAPI/AcAccount.py b/ActiveCollabAPI/AcAccount.py new file mode 100644 index 0000000..99de25b --- /dev/null +++ b/ActiveCollabAPI/AcAccount.py @@ -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) diff --git a/ActiveCollabAPI/AcAuthenticator.py b/ActiveCollabAPI/AcAuthenticator.py new file mode 100644 index 0000000..76a9bee --- /dev/null +++ b/ActiveCollabAPI/AcAuthenticator.py @@ -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()) diff --git a/ActiveCollabAPI/ACClient.py b/ActiveCollabAPI/AcClient.py similarity index 87% rename from ActiveCollabAPI/ACClient.py rename to ActiveCollabAPI/AcClient.py index 6cc400b..49d927f 100644 --- a/ActiveCollabAPI/ACClient.py +++ b/ActiveCollabAPI/AcClient.py @@ -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): diff --git a/ActiveCollabAPI/AcLoginResponse.py b/ActiveCollabAPI/AcLoginResponse.py new file mode 100644 index 0000000..4105ea8 --- /dev/null +++ b/ActiveCollabAPI/AcLoginResponse.py @@ -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] diff --git a/ActiveCollabAPI/AcProject.py b/ActiveCollabAPI/AcProject.py new file mode 100644 index 0000000..9366ada --- /dev/null +++ b/ActiveCollabAPI/AcProject.py @@ -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) diff --git a/ActiveCollabAPI/AcSession.py b/ActiveCollabAPI/AcSession.py new file mode 100644 index 0000000..60a163c --- /dev/null +++ b/ActiveCollabAPI/AcSession.py @@ -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()) \ No newline at end of file diff --git a/ActiveCollabAPI/AcSubtask.py b/ActiveCollabAPI/AcSubtask.py new file mode 100644 index 0000000..bbc5b26 --- /dev/null +++ b/ActiveCollabAPI/AcSubtask.py @@ -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) diff --git a/ActiveCollabAPI/AcToken.py b/ActiveCollabAPI/AcToken.py new file mode 100644 index 0000000..98083a4 --- /dev/null +++ b/ActiveCollabAPI/AcToken.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + + +@dataclass +class AcToken: + token: str diff --git a/ActiveCollabAPI/AcTokenAuthenticator.py b/ActiveCollabAPI/AcTokenAuthenticator.py new file mode 100644 index 0000000..1d41022 --- /dev/null +++ b/ActiveCollabAPI/AcTokenAuthenticator.py @@ -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)) diff --git a/ActiveCollabAPI/AcUser.py b/ActiveCollabAPI/AcUser.py new file mode 100644 index 0000000..214be3b --- /dev/null +++ b/ActiveCollabAPI/AcUser.py @@ -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()) diff --git a/ActiveCollabAPI/ActiveCollab.py b/ActiveCollabAPI/ActiveCollab.py new file mode 100644 index 0000000..68c2aa0 --- /dev/null +++ b/ActiveCollabAPI/ActiveCollab.py @@ -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 diff --git a/Dockerfile b/Dockerfile index e12d8b2..767211f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index 9279c70..d0804f7 100644 --- a/README.md +++ b/README.md @@ -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= - 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" -``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..29a0281 --- /dev/null +++ b/TODO.md @@ -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 diff --git a/create-subtasks.sh b/create-subtasks.sh new file mode 100755 index 0000000..c214989 --- /dev/null +++ b/create-subtasks.sh @@ -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 + diff --git a/integration_tests/__init__.py b/integration_tests/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/integration_tests/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/integration_tests/test_Integration_AcLogin.py b/integration_tests/test_Integration_AcLogin.py new file mode 100644 index 0000000..827ed95 --- /dev/null +++ b/integration_tests/test_Integration_AcLogin.py @@ -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) diff --git a/main.py b/main.py index f9e6fff..faa054e 100644 --- a/main.py +++ b/main.py @@ -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() diff --git a/sub-tasks.txt b/sub-tasks.txt index 8689bc3..2980b30 100644 --- a/sub-tasks.txt +++ b/sub-tasks.txt @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/test-config.json b/test-config.json new file mode 100644 index 0000000..c989bc4 --- /dev/null +++ b/test-config.json @@ -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" +} \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_AcAuthenticator.py b/tests/test_AcAuthenticator.py new file mode 100644 index 0000000..912e83b --- /dev/null +++ b/tests/test_AcAuthenticator.py @@ -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) diff --git a/tests/test_AcTokenAuthenticator.py b/tests/test_AcTokenAuthenticator.py new file mode 100644 index 0000000..c4dca12 --- /dev/null +++ b/tests/test_AcTokenAuthenticator.py @@ -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) + +