mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-24 03:58:49 +03:00
Update flat-manager-client
This commit is contained in:
parent
cf373c016a
commit
cc75c930fc
1 changed files with 133 additions and 39 deletions
|
@ -26,7 +26,6 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from yarl import URL
|
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from urllib.parse import urljoin, urlparse, urlsplit, urlunparse, urlunsplit
|
from urllib.parse import urljoin, urlparse, urlsplit, urlunparse, urlunsplit
|
||||||
|
@ -72,8 +71,26 @@ class ApiError(Exception):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Api call to %s failed with status %d, details: %s" % (self.url, self.status, self.body)
|
return "Api call to %s failed with status %d, details: %s" % (self.url, self.status, self.body)
|
||||||
|
|
||||||
|
class ServerApiError(ApiError):
|
||||||
|
def __init__(self, response, body):
|
||||||
|
super().__init__(response, body)
|
||||||
|
|
||||||
TENACITY_RETRY_EXCEPTIONS = (retry_if_exception_type(aiohttp.client_exceptions.ServerDisconnectedError) | retry_if_exception_type(ApiError) | retry_if_exception_type(aiohttp.client_exceptions.ServerConnectionError))
|
|
||||||
|
class FailedJobError(Exception):
|
||||||
|
def __init__(self, job):
|
||||||
|
self.job = job
|
||||||
|
|
||||||
|
def repr(self):
|
||||||
|
return {
|
||||||
|
"type": "job",
|
||||||
|
"job": self.job
|
||||||
|
}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Job failed: %s" % (self.job)
|
||||||
|
|
||||||
|
|
||||||
|
TENACITY_RETRY_EXCEPTIONS = (retry_if_exception_type(aiohttp.client_exceptions.ServerDisconnectedError) | retry_if_exception_type(ServerApiError) | retry_if_exception_type(aiohttp.client_exceptions.ServerConnectionError) | retry_if_exception_type(aiohttp.client_exceptions.ClientOSError))
|
||||||
TENACITY_STOP_AFTER = stop_after_delay(300)
|
TENACITY_STOP_AFTER = stop_after_delay(300)
|
||||||
TENACITY_WAIT_BETWEEN = wait_random_exponential(multiplier=1, max=60)
|
TENACITY_WAIT_BETWEEN = wait_random_exponential(multiplier=1, max=60)
|
||||||
|
|
||||||
|
@ -215,7 +232,7 @@ async def missing_objects(session, build_url, token, wanted):
|
||||||
@retry(
|
@retry(
|
||||||
stop=TENACITY_STOP_AFTER,
|
stop=TENACITY_STOP_AFTER,
|
||||||
wait=TENACITY_WAIT_BETWEEN,
|
wait=TENACITY_WAIT_BETWEEN,
|
||||||
retry=(retry_if_exception_type(ApiError) | retry_if_exception_type(aiohttp.client_exceptions.ServerDisconnectedError)),
|
retry=TENACITY_RETRY_EXCEPTIONS,
|
||||||
reraise=True,
|
reraise=True,
|
||||||
)
|
)
|
||||||
async def upload_files(session, build_url, token, files):
|
async def upload_files(session, build_url, token, files):
|
||||||
|
@ -228,7 +245,9 @@ async def upload_files(session, build_url, token, files):
|
||||||
writer.headers['Authorization'] = 'Bearer ' + token
|
writer.headers['Authorization'] = 'Bearer ' + token
|
||||||
resp = await session.request("post", build_url + '/upload', data=writer, headers=writer.headers)
|
resp = await session.request("post", build_url + '/upload', data=writer, headers=writer.headers)
|
||||||
async with resp:
|
async with resp:
|
||||||
if resp.status != 200:
|
if resp.status >= 500:
|
||||||
|
raise ServerApiError(resp, await resp.text())
|
||||||
|
elif resp.status != 200:
|
||||||
raise ApiError(resp, await resp.text())
|
raise ApiError(resp, await resp.text())
|
||||||
|
|
||||||
|
|
||||||
|
@ -285,11 +304,17 @@ async def upload_objects(session, repo_path, build_url, token, objects):
|
||||||
retry=TENACITY_RETRY_EXCEPTIONS,
|
retry=TENACITY_RETRY_EXCEPTIONS,
|
||||||
reraise=True,
|
reraise=True,
|
||||||
)
|
)
|
||||||
async def create_ref(session, build_url, token, ref, commit):
|
async def create_ref(session, build_url, token, ref, commit, build_log_url=None):
|
||||||
print("Creating ref %s with commit %s" % (ref, commit))
|
print("Creating ref %s with commit %s" % (ref, commit))
|
||||||
resp = await session.post(build_url + "/build_ref", headers={'Authorization': 'Bearer ' + token}, json= { "ref": ref, "commit": commit} )
|
resp = await session.post(
|
||||||
|
build_url + "/build_ref",
|
||||||
|
headers={ 'Authorization': 'Bearer ' + token },
|
||||||
|
json= { "ref": ref, "commit": commit, "build-log-url": build_log_url }
|
||||||
|
)
|
||||||
async with resp:
|
async with resp:
|
||||||
if resp.status != 200:
|
if resp.status >= 500:
|
||||||
|
raise ServerApiError(resp, await resp.text())
|
||||||
|
elif resp.status != 200:
|
||||||
raise ApiError(resp, await resp.text())
|
raise ApiError(resp, await resp.text())
|
||||||
|
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
|
@ -306,7 +331,9 @@ async def add_extra_ids(session, build_url, token, extra_ids):
|
||||||
print("Adding extra ids %s" % (extra_ids))
|
print("Adding extra ids %s" % (extra_ids))
|
||||||
resp = await session.post(build_url + "/add_extra_ids", headers={'Authorization': 'Bearer ' + token}, json= { "ids": extra_ids} )
|
resp = await session.post(build_url + "/add_extra_ids", headers={'Authorization': 'Bearer ' + token}, json= { "ids": extra_ids} )
|
||||||
async with resp:
|
async with resp:
|
||||||
if resp.status != 200:
|
if resp.status >= 500:
|
||||||
|
raise ServerApiError(resp, await resp.text())
|
||||||
|
elif resp.status != 200:
|
||||||
raise ApiError(resp, await resp.text())
|
raise ApiError(resp, await resp.text())
|
||||||
|
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
|
@ -322,6 +349,9 @@ async def add_extra_ids(session, build_url, token, extra_ids):
|
||||||
async def get_build(session, build_url, token):
|
async def get_build(session, build_url, token):
|
||||||
resp = await session.get(build_url, headers={'Authorization': 'Bearer ' + token})
|
resp = await session.get(build_url, headers={'Authorization': 'Bearer ' + token})
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
|
if resp.status >= 500:
|
||||||
|
raise ServerApiError(resp, await resp.text())
|
||||||
|
elif resp.status != 200:
|
||||||
raise ApiError(resp, await resp.text())
|
raise ApiError(resp, await resp.text())
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
return data
|
return data
|
||||||
|
@ -341,7 +371,9 @@ def reparse_job_results(job):
|
||||||
async def get_job(session, job_url, token):
|
async def get_job(session, job_url, token):
|
||||||
resp = await session.get(job_url, headers={'Authorization': 'Bearer ' + token}, json={})
|
resp = await session.get(job_url, headers={'Authorization': 'Bearer ' + token}, json={})
|
||||||
async with resp:
|
async with resp:
|
||||||
if resp.status != 200:
|
if resp.status >= 500:
|
||||||
|
raise ServerApiError(resp, await resp.text())
|
||||||
|
elif resp.status != 200:
|
||||||
raise ApiError(resp, await resp.text())
|
raise ApiError(resp, await resp.text())
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
return data
|
return data
|
||||||
|
@ -376,7 +408,7 @@ async def wait_for_job(session, job_url, token):
|
||||||
if start_after and start_after > now:
|
if start_after and start_after > now:
|
||||||
print("Waiting %d seconds before starting job" % (int(start_after - now)))
|
print("Waiting %d seconds before starting job" % (int(start_after - now)))
|
||||||
if job_status > 0 and old_job_status == 0:
|
if job_status > 0 and old_job_status == 0:
|
||||||
print("/ Job was started")
|
print("/ Job was started");
|
||||||
old_job_status = job_status
|
old_job_status = job_status
|
||||||
log = job['log']
|
log = job['log']
|
||||||
if len(log) > 0:
|
if len(log) > 0:
|
||||||
|
@ -388,9 +420,10 @@ async def wait_for_job(session, job_url, token):
|
||||||
iterations_since_change=iterations_since_change+1
|
iterations_since_change=iterations_since_change+1
|
||||||
if job_status > 1:
|
if job_status > 1:
|
||||||
if job_status == 2:
|
if job_status == 2:
|
||||||
print("\ Job completed successfully")
|
print("\\ Job completed successfully")
|
||||||
else:
|
else:
|
||||||
print("\ Job failed")
|
print("\\ Job failed")
|
||||||
|
raise FailedJobError(job)
|
||||||
return job
|
return job
|
||||||
else:
|
else:
|
||||||
iterations_since_change=4 # Start at 4 so we ramp up the delay faster
|
iterations_since_change=4 # Start at 4 so we ramp up the delay faster
|
||||||
|
@ -421,6 +454,51 @@ async def wait_for_job(session, job_url, token):
|
||||||
sleep_time=60
|
sleep_time=60
|
||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
@retry(
|
||||||
|
stop=TENACITY_STOP_AFTER,
|
||||||
|
wait=TENACITY_WAIT_BETWEEN,
|
||||||
|
retry=TENACITY_RETRY_EXCEPTIONS,
|
||||||
|
reraise=True,
|
||||||
|
)
|
||||||
|
async def wait_for_checks(session, build_url, token):
|
||||||
|
print("Waiting for checks, if any...")
|
||||||
|
while True:
|
||||||
|
resp = await session.get(build_url + "/extended", headers={'Authorization': 'Bearer ' + token})
|
||||||
|
async with resp:
|
||||||
|
if resp.status == 404:
|
||||||
|
return
|
||||||
|
if resp.status >= 500:
|
||||||
|
raise ServerApiError(resp, await resp.text())
|
||||||
|
elif resp.status != 200:
|
||||||
|
raise ApiError(resp, await resp.text())
|
||||||
|
|
||||||
|
build = await resp.json()
|
||||||
|
|
||||||
|
# wait for the repo to be validated
|
||||||
|
if build["build"]["repo_state"] == 1:
|
||||||
|
time.sleep(2)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
for check in build["checks"]:
|
||||||
|
print("Waiting for check: %s" % (check["check_name"]))
|
||||||
|
job_url = build_url + "/check/" + check["check_name"] + "/job"
|
||||||
|
await wait_for_job(session, job_url, token)
|
||||||
|
|
||||||
|
resp = await session.get(build_url + "/extended", headers={'Authorization': 'Bearer ' + token})
|
||||||
|
async with resp:
|
||||||
|
if resp.status == 404:
|
||||||
|
return
|
||||||
|
if resp.status >= 500:
|
||||||
|
raise ServerApiError(resp, await resp.text())
|
||||||
|
elif resp.status != 200:
|
||||||
|
raise ApiError(resp, await resp.text())
|
||||||
|
|
||||||
|
build = await resp.json()
|
||||||
|
for check in build["checks"]:
|
||||||
|
if check["status"] == 3:
|
||||||
|
print("\\ Check {} has failed".format(check["check_name"]))
|
||||||
|
raise FailedJobError(check)
|
||||||
|
|
||||||
@retry(
|
@retry(
|
||||||
stop=TENACITY_STOP_AFTER,
|
stop=TENACITY_STOP_AFTER,
|
||||||
|
@ -438,18 +516,23 @@ async def commit_build(session, build_url, eol, eol_rebase, token_type, wait, to
|
||||||
json['token_type'] = token_type
|
json['token_type'] = token_type
|
||||||
resp = await session.post(build_url + "/commit", headers={'Authorization': 'Bearer ' + token}, json=json)
|
resp = await session.post(build_url + "/commit", headers={'Authorization': 'Bearer ' + token}, json=json)
|
||||||
async with resp:
|
async with resp:
|
||||||
if resp.status != 200:
|
if resp.status >= 500:
|
||||||
|
raise ServerApiError(resp, await resp.text())
|
||||||
|
elif resp.status != 200:
|
||||||
raise ApiError(resp, await resp.text())
|
raise ApiError(resp, await resp.text())
|
||||||
|
|
||||||
job = await resp.json()
|
job = await resp.json()
|
||||||
job_url = keep_host(resp.headers['location'], build_url)
|
job_url = resp.headers['location'];
|
||||||
|
|
||||||
if wait:
|
if wait:
|
||||||
|
pass
|
||||||
|
|
||||||
print("Waiting for commit job")
|
print("Waiting for commit job")
|
||||||
|
await wait_for_checks(session, build_url, token)
|
||||||
job = await wait_for_job(session, job_url, token)
|
job = await wait_for_job(session, job_url, token)
|
||||||
|
|
||||||
reparse_job_results(job)
|
reparse_job_results(job)
|
||||||
job["location"] = keep_host(job_url, build_url)
|
job["location"] = job_url
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
|
||||||
|
@ -469,9 +552,17 @@ async def publish_build(session, build_url, wait, token):
|
||||||
if isinstance(body, str):
|
if isinstance(body, str):
|
||||||
body = json.loads(body)
|
body = json.loads(body)
|
||||||
|
|
||||||
if body.get("current-state") == "published":
|
current_state = body.get("current-state", None)
|
||||||
|
|
||||||
|
if current_state == "published":
|
||||||
print("the build has been already published")
|
print("the build has been already published")
|
||||||
return {}
|
return {}
|
||||||
|
elif current_state == "failed":
|
||||||
|
print("the build has failed")
|
||||||
|
raise ApiError(resp, await resp.text())
|
||||||
|
elif current_state == "validating":
|
||||||
|
print("the build is still being validated or held for review")
|
||||||
|
return {}
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -479,14 +570,14 @@ async def publish_build(session, build_url, wait, token):
|
||||||
raise ApiError(resp, await resp.text())
|
raise ApiError(resp, await resp.text())
|
||||||
|
|
||||||
job = await resp.json()
|
job = await resp.json()
|
||||||
job_url = keep_host(resp.headers['location'], build_url)
|
job_url = resp.headers['location'];
|
||||||
|
|
||||||
if wait:
|
if wait:
|
||||||
print("Waiting for publish job")
|
print("Waiting for publish job")
|
||||||
job = await wait_for_job(session, job_url, token)
|
job = await wait_for_job(session, job_url, token);
|
||||||
|
|
||||||
reparse_job_results(job)
|
reparse_job_results(job)
|
||||||
job["location"] = keep_host(job_url, build_url)
|
job["location"] = job_url
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
|
||||||
|
@ -500,7 +591,9 @@ async def purge_build(session, build_url, token):
|
||||||
print("Purging build %s" % (build_url))
|
print("Purging build %s" % (build_url))
|
||||||
resp = await session.post(build_url + "/purge", headers={'Authorization': 'Bearer ' + token}, json= {} )
|
resp = await session.post(build_url + "/purge", headers={'Authorization': 'Bearer ' + token}, json= {} )
|
||||||
async with resp:
|
async with resp:
|
||||||
if resp.status != 200:
|
if resp.status >= 500:
|
||||||
|
raise ServerApiError(resp, await resp.text())
|
||||||
|
elif resp.status != 200:
|
||||||
raise ApiError(resp, await resp.text())
|
raise ApiError(resp, await resp.text())
|
||||||
return await resp.json()
|
return await resp.json()
|
||||||
|
|
||||||
|
@ -520,7 +613,9 @@ async def create_token(session, manager_url, token, name, subject, scope, durati
|
||||||
"duration": duration,
|
"duration": duration,
|
||||||
})
|
})
|
||||||
async with resp:
|
async with resp:
|
||||||
if resp.status != 200:
|
if resp.status >= 500:
|
||||||
|
raise ServerApiError(resp, await resp.text())
|
||||||
|
elif resp.status != 200:
|
||||||
raise ApiError(resp, await resp.text())
|
raise ApiError(resp, await resp.text())
|
||||||
return await resp.json()
|
return await resp.json()
|
||||||
|
|
||||||
|
@ -543,12 +638,16 @@ async def create_command(session, args):
|
||||||
json["app-id"] = args.app_id
|
json["app-id"] = args.app_id
|
||||||
if args.public_download is not None:
|
if args.public_download is not None:
|
||||||
json["public-download"] = args.public_download
|
json["public-download"] = args.public_download
|
||||||
|
if args.build_log_url is not None:
|
||||||
|
json["build-log-url"] = args.build_log_url
|
||||||
resp = await session.post(build_url, headers={'Authorization': 'Bearer ' + args.token}, json=json)
|
resp = await session.post(build_url, headers={'Authorization': 'Bearer ' + args.token}, json=json)
|
||||||
async with resp:
|
async with resp:
|
||||||
if resp.status != 200:
|
if resp.status >= 500:
|
||||||
|
raise ServerApiError(resp, await resp.text())
|
||||||
|
elif resp.status != 200:
|
||||||
raise ApiError(resp, await resp.text())
|
raise ApiError(resp, await resp.text())
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
data["location"] = keep_host(resp.headers['location'], build_url)
|
data["location"] = resp.headers['location']
|
||||||
if not args.print_output:
|
if not args.print_output:
|
||||||
print(resp.headers['location'])
|
print(resp.headers['location'])
|
||||||
return data
|
return data
|
||||||
|
@ -566,12 +665,6 @@ def should_skip_delta(id, globs):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# work around for url_for returning http urls when flat-manager is behind a reverse proxy and aiohttp not keeping the Authorization header across redirects in version < 4
|
|
||||||
def keep_host(location, original):
|
|
||||||
loc_url = URL(location)
|
|
||||||
org_url = URL(original)
|
|
||||||
return str(loc_url.with_scheme(org_url.scheme).with_host(org_url.host).with_port(org_url.port))
|
|
||||||
|
|
||||||
def build_url_to_api(build_url):
|
def build_url_to_api(build_url):
|
||||||
parts = urlparse(build_url)
|
parts = urlparse(build_url)
|
||||||
path = os.path.dirname(os.path.dirname(parts.path))
|
path = os.path.dirname(os.path.dirname(parts.path))
|
||||||
|
@ -637,7 +730,7 @@ async def push_command(session, args):
|
||||||
|
|
||||||
# Then the refs
|
# Then the refs
|
||||||
for ref, commit in refs.items():
|
for ref, commit in refs.items():
|
||||||
await create_ref(session, args.build_url, token, ref, commit)
|
await create_ref(session, args.build_url, token, ref, commit, build_log_url=args.build_log_url)
|
||||||
|
|
||||||
# Then any extra ids
|
# Then any extra ids
|
||||||
if args.extra_id:
|
if args.extra_id:
|
||||||
|
@ -659,11 +752,11 @@ async def push_command(session, args):
|
||||||
update_job_url = build_url_to_api(args.build_url) + "/job/" + str(update_job_id)
|
update_job_url = build_url_to_api(args.build_url) + "/job/" + str(update_job_id)
|
||||||
if args.wait_update:
|
if args.wait_update:
|
||||||
print("Waiting for repo update job")
|
print("Waiting for repo update job")
|
||||||
update_job = await wait_for_job (session, update_job_url, token)
|
update_job = await wait_for_job (session, update_job_url, token);
|
||||||
else:
|
else:
|
||||||
update_job = await get_job(session, update_job_url, token)
|
update_job = await get_job(session, update_job_url, token)
|
||||||
reparse_job_results(update_job)
|
reparse_job_results(update_job)
|
||||||
update_job["location"] = keep_host(update_job_url, update_job_url)
|
update_job["location"] = update_job_url
|
||||||
|
|
||||||
data = await get_build(session, args.build_url, args.token)
|
data = await get_build(session, args.build_url, args.token)
|
||||||
if commit_job:
|
if commit_job:
|
||||||
|
@ -686,11 +779,11 @@ async def publish_command(session, args):
|
||||||
update_job_url = build_url_to_api(args.build_url) + "/job/" + str(update_job_id)
|
update_job_url = build_url_to_api(args.build_url) + "/job/" + str(update_job_id)
|
||||||
if args.wait_update:
|
if args.wait_update:
|
||||||
print("Waiting for repo update job")
|
print("Waiting for repo update job")
|
||||||
update_job = await wait_for_job(session, update_job_url, args.token)
|
update_job = await wait_for_job(session, update_job_url, args.token);
|
||||||
else:
|
else:
|
||||||
update_job = await get_job(session, update_job_url, args.token)
|
update_job = await get_job(session, update_job_url, args.token)
|
||||||
reparse_job_results(update_job)
|
reparse_job_results(update_job)
|
||||||
update_job["location"] = keep_host(update_job_url, args.build_url)
|
update_job["location"] = update_job_url
|
||||||
return job
|
return job
|
||||||
|
|
||||||
async def purge_command(session, args):
|
async def purge_command(session, args):
|
||||||
|
@ -736,6 +829,7 @@ if __name__ == '__main__':
|
||||||
create_parser.add_argument('app_id', nargs='?', help='app ID')
|
create_parser.add_argument('app_id', nargs='?', help='app ID')
|
||||||
create_parser.add_argument('--public_download', action='store_true', default=None, help='allow public read access to the build repo')
|
create_parser.add_argument('--public_download', action='store_true', default=None, help='allow public read access to the build repo')
|
||||||
create_parser.add_argument('--no_public_download', action='store_false', dest='public_download', default=None, help='allow public read access to the build repo')
|
create_parser.add_argument('--no_public_download', action='store_false', dest='public_download', default=None, help='allow public read access to the build repo')
|
||||||
|
create_parser.add_argument('--build-log-url', help='Set URL of the build log for the whole build')
|
||||||
create_parser.set_defaults(func=create_command)
|
create_parser.set_defaults(func=create_command)
|
||||||
|
|
||||||
push_parser = subparsers.add_parser('push', help='Push to repo manager')
|
push_parser = subparsers.add_parser('push', help='Push to repo manager')
|
||||||
|
@ -757,6 +851,7 @@ if __name__ == '__main__':
|
||||||
push_parser.add_argument('--end-of-life', help='Set end of life')
|
push_parser.add_argument('--end-of-life', help='Set end of life')
|
||||||
push_parser.add_argument('--end-of-life-rebase', help='Set new ID which will supercede the current one')
|
push_parser.add_argument('--end-of-life-rebase', help='Set new ID which will supercede the current one')
|
||||||
push_parser.add_argument('--token-type', help='Set token type', type=int)
|
push_parser.add_argument('--token-type', help='Set token type', type=int)
|
||||||
|
push_parser.add_argument('--build-log-url', help='Set URL of the build log for each uploaded ref')
|
||||||
push_parser.set_defaults(func=push_command)
|
push_parser.set_defaults(func=push_command)
|
||||||
|
|
||||||
commit_parser = subparsers.add_parser('commit', help='Commit build')
|
commit_parser = subparsers.add_parser('commit', help='Commit build')
|
||||||
|
@ -834,7 +929,7 @@ if __name__ == '__main__':
|
||||||
# Something called sys.exit(), lets just exit
|
# Something called sys.exit(), lets just exit
|
||||||
res = 1
|
res = 1
|
||||||
raise # Pass on regular exit callse
|
raise # Pass on regular exit callse
|
||||||
except ApiError as e:
|
except (ApiError, FailedJobError) as e:
|
||||||
eprint(str(e))
|
eprint(str(e))
|
||||||
output = {
|
output = {
|
||||||
"command": args.subparser_name,
|
"command": args.subparser_name,
|
||||||
|
@ -875,4 +970,3 @@ if __name__ == '__main__':
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
f.close()
|
f.close()
|
||||||
exit(res)
|
exit(res)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue