Browse Source

Move API description to metadata to deprecate GiteaRepo subclass.

master
poikilos 3 years ago
parent
commit
d29016227a
  1. 365
      utilities/enissue.py

365
utilities/enissue.py

@ -62,6 +62,71 @@ def error(*args, **kwargs):
# https://api.github.com/repos/poikilos/EnlivenMinetest/issues/475/timeline # https://api.github.com/repos/poikilos/EnlivenMinetest/issues/475/timeline
verbose = False verbose = False
default_options = {
# 'repo_url': "https://api.github.com/repos/poikilos/EnlivenMinetest",
'repo_url': "https://github.com/poikilos/EnlivenMinetest",
}
sites_users_repos_meta = {
'https://api.github.com': {
'poikilos': {
'EnlivenMinetest': {
'repository_id': "80873867"
}
}
}
}
'''
# ^ Get repo metadata for known repos such as via:
site_users_repos_meta = sites_users_repos_meta.get(instance_url)
if site_users_repos_meta is not None:
user_repos_meta = site_users_repos_meta.get(remote_user)
if user_repos_meta is not None:
repo_meta = user_repos_meta.get(repo_name)
if repo_meta is not None:
if repository_id is None:
repository_id = repo_meta.get('repository_id')
'''
github_defaults = {
'repository_id': "80873867",
'instance_url': "https://api.github.com",
'api_repo_url_fmt': "{instance_url}/repos/{ru}/{rn}",
'api_issue_url_fmt': "{instance_url}/repos/{ru}/{rn}/issues/{issue_no}",
'search_issues_url_fmt': "{instance_url}/search/issues?q=repo%3A{ru}/{rn}+",
# 'base_query_fmt': "?q=repo%3A{ru}/{rn}+",
'search_results_key': "items",
'page_size': 30,
'c_issues_name_fmt': "issues_page={p}{q}.json",
'c_issue_name_fmt': "{issue_no}.json",
'default_query': {'state':'open'},
'hide_events': ['renamed', 'assigned'],
'api_comments_url_fmt': "{instance_url}/repos/{ru}/{rn}/issues/comments",
}
gitea_defaults = {
'repository_id': None,
'api_repo_url_fmt': "{instance_url}/api/v1/repos/{ru}/{rn}",
'api_issue_url_fmt': "{instance_url}/api/v1/repos/{ru}/{rn}/issues/{issue_no}",
'search_issues_url_fmt': "{instance_url}/api/v1/search/issues?q=repo%3A{ru}/{rn}+",
# 'base_query_fmt': "?q=repo%3A{ru}/{rn}+", # TODO: Change for Gitea ??
'search_results_key': "items", # TODO: Change for Gitea ??
'page_size': 30, # TODO: Change for Gitea ??
'c_issues_name_fmt': "issues_page={p}{q}.json",
'c_issue_name_fmt': "{issue_no}.json",
'default_query': {'state':'open'}, # TODO: Change for Gitea ??
'hide_events': ['renamed', 'assigned'],
'api_comments_url_fmt': "{instance_url}/api/v1/repos/{ru}/{rn}/issues/comments",
}
# API documentation:
# https://docs.gitea.io/en-us/api-usage/ says:
# > API Reference guide is auto-generated by swagger and available on: https://gitea.your.host/api/swagger or on [gitea demo instance](https://try.gitea.io/api/swagger)
# > The OpenAPI document is at: https://gitea.your.host/swagger.v1.json
apis = {}
apis["GitHub"] = github_defaults
apis["Gitea"] = gitea_defaults
def debug(msg): def debug(msg):
@ -133,6 +198,7 @@ modes = {
"examples": [" page 2", " page 2 --closed"] "examples": [" page 2", " page 2 --closed"]
}, },
"<#>": { "<#>": {
"parent": "issue",
"help": "Specify an issue number to see details.", "help": "Specify an issue number to see details.",
"examples": [" 1"] "examples": [" 1"]
}, },
@ -147,7 +213,8 @@ match_all_labels = []
def toSubQueryValue(value): def toSubQueryValue(value):
''' '''
Convert the value to one that will fit in a Convert the value to one that will fit in a
key+urlencoded(colon)+value string for GitHub queries. key+urlencoded(colon)+value="+" string (can end in plus, so leave
it on the end to easily add more terms later) for GitHub queries.
This function is copied to multiple scripts so they have no This function is copied to multiple scripts so they have no
dependencies: dependencies:
@ -188,54 +255,47 @@ class Repo:
profile = os.environ['HOME'] profile = os.environ['HOME']
os_user = os.environ.get('USER') os_user = os.environ.get('USER')
def __init__( def __init__(
self, self,
remote_user="poikilos", options,
repo_name="EnlivenMinetest",
repository_id="80873867",
instance_url="https://api.github.com",
api_repo_url_fmt="{instance_url}/repos/{ru}/{rn}",
api_issue_url_fmt="{instance_url}/repos/{ru}/{rn}/issues/{issue_no}",
search_issues_url_fmt="{instance_url}/search/issues?q=repo%3A{ru}/{rn}+",
search_results_key="items",
page_size=30,
c_issues_name_fmt="issues_page={p}{q}.json",
c_issue_name_fmt="{issue_no}.json",
default_query={'state':'open'},
hide_events=['renamed', 'assigned'],
caches_path=None, caches_path=None,
api_comments_url_fmt="{instance_url}/repos/{ru}/{rn}/issues/comments",
): ):
''' '''
Keyword arguments: Keyword arguments:
remote_user -- The repo user may be used in api_repo_url_fmt. options -- The options dict have any of the following keys (any
repo_name -- The repo name may be used in api_repo_url_fmt. that aren't set will be detected based on the URL--if
api_repo_url_fmt -- The format string where {ru} is where a repo there is an api name that corresponds to your site's
user goes, and {rn} is where a repo name API in the apis global dict):
goes, is used for the format of repo_url -- This is required. It can be an API or web URL
self.repo_url. as long as it ends with username/reponame (except where
api_issue_url_fmt -- a format string where {issue_url} is there is no username in the URL).
determined by api_repo_url_fmt and remote_user -- The repo user may be used in api_repo_url_fmt.
{issue_no} is where an issue number goes. repo_name -- The repo name may be used in api_repo_url_fmt.
api_comments_url_fmt -- Set the comments URL format (see the api_repo_url_fmt -- The format string where {ru} is where a repo
default for an example). user goes, and {rn} is where a repo name
page_size -- This must be set to the page size that is goes, is used for the format of
compatible with the URL in api_repo_url_fmt, such self.repo_url.
as exactly 30 for GitHub. api_issue_url_fmt -- a format string where {issue_url} is
c_issues_name_fmt -- This converts a URL to a cache filename, determined by api_repo_url_fmt and
where {p} is the page number and {q} is any {issue_no} is where an issue number goes.
additional query such as "&state=closed". api_comments_url_fmt -- Set the comments URL format (see the
c_issue_name_fmt -- This converts a URL to a cache filename, default for an example).
where {issue_no} is the issue number. page_size -- This must be set to the page size that is
default_query -- This dictionary must contain all URL query compatible with the URL in api_repo_url_fmt, such
parameters that the API assumes and that don't as exactly 30 for GitHub.
need to be provided in the URL. c_issues_name_fmt -- This converts a URL to a cache filename,
hide_events -- Do not show these event types in an issue's where {p} is the page number and {q} is any
timeline. additional query such as "&state=closed".
search_results_key -- If the URL described by c_issue_name_fmt -- This converts a URL to a cache filename,
search_issues_url_fmt returns a dict, specify the key in where {issue_no} is the issue number.
the dict that is a list of issues. default_query -- This dictionary must contain all URL query
parameters that the API assumes and that don't
need to be provided in the URL.
hide_events -- Do not show these event types in an issue's
timeline.
search_results_key -- If the URL described by
search_issues_url_fmt returns a dict, specify the key in
the dict that is a list of issues.
caches_path -- Store cached json files here: Specifically, in an caches_path -- Store cached json files here: Specifically, in an
"issues" directory or other directory under the user and "issues" directory or other directory under the user and
repo directory. For example, if caches_path is None and uses repo directory. For example, if caches_path is None and uses
@ -245,7 +305,81 @@ class Repo:
"~/.cache/enissue/poikilos/EnlivenMinetest/issues". To set "~/.cache/enissue/poikilos/EnlivenMinetest/issues". To set
it later, use the setCachesPath method. it later, use the setCachesPath method.
''' '''
repo_url = options.get('repo_url')
debug("* using URL {}".format(repo_url))
if repo_url is None:
raise ValueError("repo_url is required (API or web URL).")
if repo_url.endswith(".git"):
repo_url = repo_url[:-4]
urlParts = repo_url.split("/")
remote_user = urlParts[-2]
self.api_id = options.get('api_id')
if urlParts[-2] == "repo.or.cz":
# Such as https://repo.or.cz/minetest_treasurer.git
remote_user = "almikes@aol.com" # Wuzzy2
self.api_id = "git_instaweb"
repo_name = urlParts[-1]
if self.api_id is None:
if len(urlParts) > 2:
if "github.com" in urlParts[2]:
# [0] is https:
# [1] is '' (because of //)
self.api_id = "GitHub"
debug("* detected GitHub in {}".format(urlParts))
else:
debug("* no api detected in {}[2]".format(urlParts))
else:
debug("* no generic urlParts were found in "
"".format(urlParts))
else:
debug("* using specified API: {}".format(self.api_id))
if self.api_id is None:
self.api_id = "Gitea"
error(" * assuming API is {}".format(self.api_id))
if self.api_id is None:
raise RuntimeError("api_id is not set")
api_meta = apis.get(self.api_id)
if api_meta is None:
raise NotImplementedError("{} api_id is not implemented"
"".format(self.api_id))
for k, v in api_meta.items():
# Set it to the default if it is None:
if options.get(k) is None:
options[k] = v
debug("* constructing {} Repo".format(self.api_id))
debug(" * detected remote_user \"{}\" in url"
"".format(remote_user))
debug(" * detected repo_name \"{}\" in url"
"".format(repo_name))
if self.api_id == "Gitea":
instance_url = "/".join(urlParts[:-2])
debug(" * detected Gitea url " + instance_url)
else:
instance_url = options.get('instance_url')
if instance_url is None:
raise NotImplementedError("Detecting the instance_url"
" is not implemented for"
" {}".format(self.api_id))
debug(" * detected instance_url " + instance_url)
# NOTE: self.instance_url is set by super __init__ below.
# base_query_fmt = options['base_query_fmt']
# search_issues_url_fmt = \
# "{instance_url}/api/v1/repos/issues/search"+base_query_fmt
self.repository_id = options.get('repository_id')
site_users_repos_meta = sites_users_repos_meta.get(instance_url)
if site_users_repos_meta is not None:
user_repos_meta = site_users_repos_meta.get(remote_user)
if user_repos_meta is not None:
repo_meta = user_repos_meta.get(repo_name)
if repo_meta is not None:
if self.repository_id is None:
self.repository_id = \
repo_meta.get('repository_id')
self.instance_url = instance_url self.instance_url = instance_url
self.rateLimitFmt = ("You may be able to view the issues" self.rateLimitFmt = ("You may be able to view the issues"
" at the html_url, and a login may be" " at the html_url, and a login may be"
" required. The URL \"{}\" is not" " required. The URL \"{}\" is not"
@ -258,25 +392,26 @@ class Repo:
self.repo_name = repo_name self.repo_name = repo_name
self.setCachesPath(caches_path) self.setCachesPath(caches_path)
self.search_results_key = search_results_key self.search_results_key = options.get('search_results_key')
self.page = None self.page = options.get('page')
self.repository_id = repository_id self.c_issue_name_fmt = options['c_issue_name_fmt']
self.c_issue_name_fmt = c_issue_name_fmt self.api_repo_url_fmt = options['api_repo_url_fmt']
self.api_repo_url_fmt = api_repo_url_fmt self.api_issue_url_fmt = options['api_issue_url_fmt']
self.api_issue_url_fmt = api_issue_url_fmt
self.repo_url = self.api_repo_url_fmt.format( self.repo_url = self.api_repo_url_fmt.format(
instance_url=instance_url, instance_url=instance_url,
ru=remote_user, ru=remote_user,
rn=repo_name, rn=repo_name,
) )
self.search_issues_url = search_issues_url_fmt.format( self.search_issues_url_fmt = \
options.get('search_issues_url_fmt')
self.search_issues_url = self.search_issues_url_fmt.format(
instance_url=instance_url, instance_url=instance_url,
ru=remote_user, ru=remote_user,
rn=repo_name, rn=repo_name,
) )
self.api_comments_url_fmt = api_comments_url_fmt self.api_comments_url_fmt = options['api_comments_url_fmt']
self.comments_url = api_comments_url_fmt.format( self.comments_url = self.api_comments_url_fmt.format(
instance_url=instance_url, instance_url=instance_url,
ru=remote_user, ru=remote_user,
rn=repo_name, rn=repo_name,
@ -284,15 +419,16 @@ class Repo:
self.issues_url = self.repo_url + "/issues" self.issues_url = self.repo_url + "/issues"
self.labels_url = self.repo_url + "/labels" self.labels_url = self.repo_url + "/labels"
self.page_size = page_size self.page_size = options['page_size']
self.log_prefix = "@ " self.log_prefix = "@ "
self.c_issues_name_fmt = c_issues_name_fmt self.c_issues_name_fmt = options['c_issues_name_fmt']
self.label_ids = [] # all label ids in the repo self.label_ids = [] # all label ids in the repo
self.labels = [] # all labels in the repo self.labels = [] # all labels in the repo
self.default_query = default_query self.default_query = options['default_query']
self.hide_events = hide_events self.hide_events = options['hide_events']
self.issues = None self.issues = None
self.last_query_s = None
def setCachesPath(self, caches_path): def setCachesPath(self, caches_path):
''' '''
@ -450,6 +586,8 @@ class Repo:
else: else:
debug(" There was no custom query.") debug(" There was no custom query.")
self.last_query_s = query_s
if os.path.isfile(c_path): if os.path.isfile(c_path):
# See <https://stackoverflow.com/questions/7430928/ # See <https://stackoverflow.com/questions/7430928/
@ -511,7 +649,10 @@ class Repo:
response = request.urlopen(query_s) response = request.urlopen(query_s)
except HTTPError as e: except HTTPError as e:
if "Error 410" in str(e): if "Error 410" in str(e):
return None, "The issue was deleted." return (
None,
"The issue was apparently deleted (response 410)."
)
msg = str(e) + ": " + self.rateLimitFmt.format(query_s) msg = str(e) + ": " + self.rateLimitFmt.format(query_s)
return None, msg return None, msg
response_s = decode_safe(response.read()) response_s = decode_safe(response.read())
@ -575,6 +716,10 @@ class Repo:
c_path = None c_path = None
if "?" in url: if "?" in url:
raise NotImplementedError("getCachedJsonDict can't query") raise NotImplementedError("getCachedJsonDict can't query")
# If this is implemented, use:
# c_issues_name_fmt
# Since even the first page should have "page" or something
# to denote there are potentially multiple pages.
''' '''
elif url.startswith(self.comments_url): elif url.startswith(self.comments_url):
# This code is not necessary since it startswith # This code is not necessary since it startswith
@ -798,6 +943,7 @@ class Repo:
cmts_data = self.getCachedJsonDict( cmts_data = self.getCachedJsonDict(
issue_data["comments_url"], issue_data["comments_url"],
refresh=refresh, refresh=refresh,
quiet=True,
) )
data = cmts_data data = cmts_data
@ -1039,77 +1185,25 @@ class Repo:
return {'issue':matching_issue, 'count':match_count} return {'issue':matching_issue, 'count':match_count}
class GiteaRepo(Repo):
def __init__(
self,
repo_url,
# instance_url="https://api.github.com",
# repository_id="80873867",
# api_repo_url_fmt="{instance_url}/repos/{ru}/{rn}",
# api_issue_url_fmt="{instance_url}/repos/{ru}/{rn}/issues/{issue_no}",
# search_issues_url_fmt="{instance_url}/search/issues?q=repo%3A{ru}/{rn}+",
# search_results_key="items",
page_size=30,
# c_issues_name_fmt="issues_page={p}{q}.json",
# c_issue_name_fmt="{issue_no}.json",
default_query={'state':'open'},
hide_events=['renamed', 'assigned'],
caches_path=None,
# api_comments_url_fmt="{instance_url}/repos/{ru}/{rn}/issues/comments",
):
if repo_url.endswith(".git"):
repo_url = repo_url[:-4]
urlParts = repo_url.split("/")
remote_user = urlParts[-2]
repo_name = urlParts[-1]
debug("* constructing GiteaRepo")
debug(" * detected remote_user \"{}\" in url"
"".format(remote_user))
debug(" * detected repo_name \"{}\" in url"
"".format(repo_name))
instance_url = "/".join(urlParts[:-2])
debug(" * detected Gitea url " + instance_url)
# NOTE: self.instance_url is set by super __init__ below.
base_query_fmt = "?q=repo%3A{ru}/{rn}+"
search_issues_url_fmt = \
"{instance_url}/api/v1/repos/issues/search"+base_query_fmt
Repo.__init__(
self,
remote_user=remote_user,
repo_name=repo_name,
repository_id=None,
instance_url=instance_url,
api_repo_url_fmt="{instance_url}/api/v1/repos/{ru}/{rn}",
api_issue_url_fmt="{instance_url}/api/v1/repos/{ru}/{rn}/issues/{issue_no}",
search_issues_url_fmt=search_issues_url_fmt,
search_results_key="items", # TODO: Change for Gitea ??
page_size=page_size, # TODO: Change for Gitea ??
default_query=default_query, # TODO: Change for Gitea ??
hide_events=hide_events,
caches_path=caches_path,
api_comments_url_fmt="{instance_url}/api/v1/repos/{ru}/{rn}/issues/comments",
)
# API documentation:
# https://docs.gitea.io/en-us/api-usage/ says:
# > API Reference guide is auto-generated by swagger and available on: https://gitea.your.host/api/swagger or on [gitea demo instance](https://try.gitea.io/api/swagger)
# > The OpenAPI document is at: https://gitea.your.host/swagger.v1.json
def main(): def main():
global verbose global verbose
mode = None mode = None
repo = Repo()
prev_arg = None prev_arg = None
issue_no = None issue_no = None
state = repo.default_query.get('state') state = None
options = {} options = {}
for k,v in default_options.items():
options[k] = v
search_terms = [] search_terms = []
SEARCH_COMMANDS = ['find', 'AND'] SEARCH_COMMANDS = ['find', 'AND'] # CLI-only commands
caches_path = None caches_path = None
logic = {} logic = {} # CLI-only values
save_key = None save_key = None
collect_options = ['--repo-url'] # Repo init data
collect_logic = ['--copy-meta-to', '--db-type', '--db-user', collect_logic = ['--copy-meta-to', '--db-type', '--db-user',
'--db-password', '--cache-base'] '--db-password', '--cache-base']
# ^ CLI arguments override default_options. For example:
# - repo_url is initially set to default_options['repo_url']
for i in range(1, len(sys.argv)): for i in range(1, len(sys.argv)):
arg = sys.argv[i] arg = sys.argv[i]
isValue = False isValue = False
@ -1129,7 +1223,7 @@ def main():
try: try:
i = int(arg) i = int(arg)
if prev_arg == "page": if prev_arg == "page":
repo.page = i options['page'] = i
isValue = True isValue = True
else: else:
if issue_no is not None: if issue_no is not None:
@ -1160,7 +1254,9 @@ def main():
usage() usage()
exit(0) exit(0)
elif arg in collect_logic: elif arg in collect_logic:
save_key = arg.strip("-") save_key = arg.strip("-").replace("-", "_")
elif arg in collect_options:
save_option = arg.strip("-").replace("-", "_")
elif arg.startswith("--"): elif arg.startswith("--"):
usage() usage()
error("Error: The argument \"{}\" is not valid" error("Error: The argument \"{}\" is not valid"
@ -1189,6 +1285,9 @@ def main():
elif save_key is not None: elif save_key is not None:
logic[save_key] = arg logic[save_key] = arg
save_key = None save_key = None
elif save_option is not None:
options[save_option] = arg
save_option = None
elif arg != "page": elif arg != "page":
mode = "list" mode = "list"
match_all_labels.append(arg) match_all_labels.append(arg)
@ -1197,6 +1296,10 @@ def main():
# It is not a command that will determine meaning for the # It is not a command that will determine meaning for the
# next var. # next var.
prev_arg = None prev_arg = None
debug("options: {}".format(options))
repo = Repo(options)
if mode is None: if mode is None:
if len(match_all_labels) > 1: if len(match_all_labels) > 1:
mode = "list" mode = "list"
@ -1205,6 +1308,9 @@ def main():
if save_key is not None: if save_key is not None:
raise ValueError("--{} requires a space then a value." raise ValueError("--{} requires a space then a value."
"".format(save_key)) "".format(save_key))
if save_option is not None:
raise ValueError("--{} requires a space then a value."
"".format(save_option))
caches_path = logic.get('cache-base') caches_path = logic.get('cache-base')
valid_modes = ["issue"] valid_modes = ["issue"]
print("command metadata: {}".format(logic)) print("command metadata: {}".format(logic))
@ -1243,10 +1349,12 @@ def main():
# TODO: get labels another way, and make this conditional: # TODO: get labels another way, and make this conditional:
# if mode == "list": # if mode == "list":
msg = None msg = None
if (mode != "issue") and (state != repo.default_query.get('state')): if (mode != "issue"):
query = { query = None
'state': state if (state != repo.default_query.get('state')):
} query = {
'state': state
}
results, msg = repo.load_issues(options, query=query, results, msg = repo.load_issues(options, query=query,
search_terms=search_terms) search_terms=search_terms)
debug("* done load_issues for list") debug("* done load_issues for list")
@ -1280,7 +1388,9 @@ def main():
" operation will be attempted without it." " operation will be attempted without it."
" Success will depend on your database type and" " Success will depend on your database type and"
" settings.") " settings.")
dstRepo = GiteaRepo(dstRepoUrl) dstRepo = Repo({
'repo_url': dstRepoUrl,
})
# print("* rewriting Gitea issue {}...".format(issue_no)) # print("* rewriting Gitea issue {}...".format(issue_no))
sys.exit(0) # Change based on return of the method. sys.exit(0) # Change based on return of the method.
@ -1318,8 +1428,11 @@ def main():
refresh=False, refresh=False,
) )
# ^ Never refresh, since that would already have been done. # ^ Never refresh, since that would already have been done.
if state != "open": state_msg = repo.default_query.get('state')
print("(showing {} issue(s))".format(state.upper())) if state_msg is None:
state_msg = repo.last_query_s
if state_msg != "open":
print("(showing {} issue(s))".format(state_msg.upper()))
# ^ such as CLOSED # ^ such as CLOSED
else: else:
debug("* There is no matching_issue; matching manually...") debug("* There is no matching_issue; matching manually...")

Loading…
Cancel
Save