diff --git a/.pydocstyle b/.pydocstyle new file mode 100755 index 0000000000000000000000000000000000000000..8839ea9e9b8c25d812682bfc9a581945a46c0a92 --- /dev/null +++ b/.pydocstyle @@ -0,0 +1,8 @@ +[pydocstyle] +ignore = D104,D105,D107,D203,D204,D213 +# D104: Missing docstring in public package +# D105 Missing docstring in magic method +# D107 Missing docstring in __init__ +# D203 1 blank line required before class docstring +# D204 1 blank line required after class docstring (found 0) +# D213 Multi-line docstring summary should start at the second line \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000000000000000000000000000000000000..a71feb49d9695b224b1a9b153da4be2d3f7e85d0 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,4 @@ +[MESSAGES CONTROL] +disable=no-self-use,too-many-instance-attributes + +good-names=id, i diff --git a/papersurfer/papersurfer.py b/papersurfer/papersurfer.py index 89cb9ee5b34c8a0e9fe655be84cb357c96216722..5165923dee5d98a09d83f6f6d2d9512ce66c7d0e 100644 --- a/papersurfer/papersurfer.py +++ b/papersurfer/papersurfer.py @@ -1,4 +1,4 @@ -""""Paper surfer - browse papers posted on the mattermost channel. +"""Paper surfer - browse papers posted on the mattermost channel. UI: @@ -16,6 +16,7 @@ from functools import partial import json import time import os +import sys import requests import mattermostdriver import urwid @@ -28,7 +29,7 @@ class ConfigError(Exception): @dataclass class PostDTO: - """"Encapsulate Mattermost Posts.""" + """Encapsulate Mattermost Posts.""" id: str message: str reporter: str @@ -40,7 +41,7 @@ class PostDTO: @dataclass class PaperDTO: - """"Encapsulate Paper meta data.""" + """Encapsulate Paper meta data.""" author: str authors: str title: str @@ -52,20 +53,24 @@ class PaperDTO: class Bibtex: + """Interface for bibtex string.""" def entry_from_doi(self, doi): + """Get bibtex string for doi.""" return Doi().get_bibtex(doi) def bib_from_dois(self, dois): + """Get bibtex string for mulitple dois.""" return "\n".join([Doi().get_bibtex(doi) for doi in dois]) class Doi: - """Interface w/ the doi.org api""" + """Interface w/ the doi.org api.""" def get_doi_link(self, doi): """Assemble doi link.""" return f"http://doi.org/{doi}" def load_doi_data(self, doi): + """Load data for doi.""" headers = { 'Accept': 'application/json', } @@ -73,7 +78,7 @@ class Doi: headers=headers).content def parse_doi_json(self, jsoncontent): - """Tranform doi json to PaperDTO""" + """Tranform doi json to PaperDTO.""" info = json.loads(jsoncontent) with open("debug.json", "w") as file: @@ -87,8 +92,7 @@ class Doi: if "author" in info else "Authors N/A") title = (info['title'] - if "title" in info - and isinstance(info['title'], str) + if "title" in info and isinstance(info['title'], str) else "Title N/A") journal = (info['publisher'] if "publisher" in info @@ -105,12 +109,14 @@ class Doi: slug) def get_bibtex(self, doi): + """Get bibtex string for doi.""" headers = { 'Accept': 'text/bibliography; style=bibtex', } return requests.get(f'http://dx.doi.org/{doi}', headers=headers).text def get_info(self, doi): + """Get information for doi.""" try: jsoncontent = self.load_doi_data(doi) data = self.parse_doi_json(jsoncontent) @@ -132,6 +138,7 @@ class Doi: class Mattermost: """Provide a simplified interaction w/ mattermost api.""" def __init__(self, url, channelname, username, password): + self.msgs = [] self.mattermost = mattermostdriver.Driver({ 'url': url, 'login_id': username, @@ -155,13 +162,14 @@ class Mattermost: self.reporters = {} def get_channel(self, channelname): - """"Try to find the paper channel by display name.""" - mm = self.mattermost - teams = [team["id"] for team in mm.teams.get_user_teams("me")] + """Try to find the paper channel by display name.""" + teamapi = self.mattermost.teams + channelapi = self.mattermost.channels + teams = [team["id"] for team in teamapi.get_user_teams("me")] channels = [] for team in teams: teamchannels = [channel for channel - in mm.channels.get_channels_for_user("me", team) + in channelapi.get_channels_for_user("me", team) if channel["display_name"] == channelname] channels.extend(teamchannels) @@ -171,22 +179,20 @@ class Mattermost: raise ConfigError return channels[0]["id"] - def get_reporter(self, id): + def get_reporter(self, userid): """Load user from mattermost api and cache.""" - if id not in self.reporters: - self.reporters[id] = self.mattermost.users.get_user(id)["username"] + userapi = self.mattermost.users + if userid not in self.reporters: + self.reporters[userid] = userapi.get_user(userid)["username"] - return self.reporters[id] + return self.reporters[userid] def retrieve_all_messages(self): """Retrieve all messages from mattermost, unfiltered for papers.""" posts = self.mattermost.posts.get_posts_for_channel(self.channel) - return [PostDTO( - id=m['id'], - message=m['message'], - reporter=self.get_reporter(m['user_id']), - doi=Doi().extract_doi(m['message']), - ) + return [PostDTO(id=m['id'], message=m['message'], + reporter=self.get_reporter(m['user_id']), + doi=Doi().extract_doi(m['message']),) for m in posts['posts'].values()] def filter_incoming(self, posts): @@ -200,6 +206,7 @@ class Mattermost: return self.msgs def check_doi_exits(self, doi): + """Check for doi in current paper list.""" doi_needle = Doi().extract_doi(doi) msg_found = [msg for msg in self.msgs if Doi().extract_doi(msg.doi) == doi_needle] @@ -212,12 +219,15 @@ class Mattermost: or needle.lower() in m.reporter.lower()] def post(self, message): + """Post message to thread.""" self.mattermost.posts.create_post({"channel_id": self.channel, "message": message}) class PrettyButton(urwid.WidgetWrap): + """Prettified urwid Button.""" def __init__(self, label, on_press=None, user_data=None): + self.label = "" self.text = urwid.Text("") self.set_label(label) self.widget = urwid.AttrMap(self.text, '', 'highlight') @@ -226,21 +236,26 @@ class PrettyButton(urwid.WidgetWrap): self._hidden_btn = urwid.Button(f"hidden {self.label}", on_press, user_data) - super(self.__class__, self).__init__(self.widget) + super().__init__(self.widget) def selectable(self): + """Make button selectable.""" return True def keypress(self, *args, **kw): + """Handle keypresses.""" return self._hidden_btn.keypress(*args, **kw) def mouse_event(self, *args, **kw): + """Handle mouse events.""" return self._hidden_btn.mouse_event(*args, **kw) def get_label(self): + """Return current input label.""" return self.label def set_label(self, label): + """Return current input label.""" self.label = label self.text.set_text(f"[ {label} ]") @@ -308,16 +323,19 @@ class Papersurfer: self.mainloop.run() def h_unhandled_input(self, key): + """Handle keyboard input not otherwise handled.""" if key == "esc": raise urwid.ExitMainLoop() def load_list(self, _loop, _data): + """Load and display paper list.""" body = [self.list_item(paper) for paper in self.mtm.retrieve()] self.listcontent.clear() self.listcontent.extend(body) self.mainloop.widget = self.top def loading_indicator(self): + """Create loading indicator dialog.""" body_text = urwid.Text("Loading...", align='center') body_filler = urwid.Filler(body_text, valign='middle') body_padding = urwid.Padding( @@ -329,6 +347,7 @@ class Papersurfer: return urwid.Frame(body_padding) def details_popup(self, paper): + """Create Dialog with paper details.""" header_text = urwid.Text(('banner', 'Paper details'), align='center') header = urwid.AttrMap(header_text, 'banner') @@ -391,7 +410,7 @@ class Papersurfer: return pile def updscrn(self): - """"Update (redraw) screen.""" + """Update (redraw) screen.""" self.mainloop.draw_screen() def onchange(self, _, needle): @@ -402,6 +421,7 @@ class Papersurfer: for paper in self.mtm.get_filtered(needle)]) def running_export(self, state): + """Set exporting state.""" btn = self.exportbutton label = btn.get_label() running_indicator = " (running...)" @@ -422,6 +442,7 @@ class Papersurfer: self.running_export(False) def export_to_bibtex(self): + """Export current filtered list to bibtex file.""" papers = self.mtm.get_filtered(self.filter) dois = [paper.doi for paper in papers] string = Bibtex().bib_from_dois(dois) @@ -455,13 +476,15 @@ class Papersurfer: self.mainloop.widget = self.details_popup(paper) def h_close_dialog(self, _): + """Handle close dialog button.""" self.close_dialog() def close_dialog(self): + """Close currently open dialog.""" self.mainloop.widget = self.top def open_submit_paper(self, _): - + """Open submit paper dialog.""" self._pile = urwid.Pile( [ PostDialog(self.mtm, close=self.h_close_dialog) @@ -494,7 +517,6 @@ def get_config_file_paths(): >>> type(get_config_file_paths()) <class 'list'> """ - env = os.environ xdg_home = None if 'XDG_CONFIG_HOME' in env: @@ -517,6 +539,7 @@ def get_config_file_paths(): def interactive_configuration(): + """Query user for credentials.""" url = input("Mattermost URL (eg. mattermost.example.net): ") channel = input("Channel (eg. Paper Club): ") username = input("Username (same as mattermost login, " @@ -526,7 +549,8 @@ def interactive_configuration(): class PostDialog(urwid.WidgetWrap): - """ + """Dialog to submit a new paper to mattermost thread. + UI: DOI: [ _________________] Generated Message: @@ -535,6 +559,8 @@ class PostDialog(urwid.WidgetWrap): [Submit] [Close] """ def __init__(self, mattermost, close): + self.doi = None + self.msg = None self.mattermost = mattermost self.close = close self.doi_input = urwid.Edit("Doi: ") @@ -558,21 +584,21 @@ class PostDialog(urwid.WidgetWrap): right=1 ) body = urwid.LineBox(body_padding) - frame = urwid.Frame( - body, - header=urwid.Text("Submit new paper to list"), - ) + frame = urwid.Frame(body, + header=urwid.Text("Submit new paper to list")) self.widget = frame - super(self.__class__, self).__init__(self.widget) + super().__init__(self.widget) def submit(self, _): + """Submit post to thread.""" if not self.mattermost.check_doi_exits(self.doi): self.mattermost.post(self.msg) self.close(_) def create_mgs(self, paper): + """Format post message.""" msg = f"""\ {paper.title} {paper.authors} @@ -581,6 +607,7 @@ class PostDialog(urwid.WidgetWrap): return msg def h_input(self, _, doi): + """Handle doi input field.""" self.doi_result.set_text("... loading ...") self.doi = None self.msg = None @@ -602,8 +629,8 @@ class PostDialog(urwid.WidgetWrap): def parse_args(): """Parse command line arguments and config file.""" - parser = configargparse.ArgParser() - parser._default_config_files = get_config_file_paths() + parser = configargparse.ArgParser( + default_config_files=get_config_file_paths()) parser.add("-w", "--write-out-config-file", help="takes the current command line args and writes them out " "to a config file at the given path", @@ -630,7 +657,7 @@ def parse_args(): Mattermost(url, channel, username, password) except ConfigError: print("Failed to validate configuration, exiting.") - exit(1) + sys.exit(1) options.url = url options.channel = channel @@ -648,7 +675,7 @@ def parse_args(): time.sleep(2) else: parser.print_help() - exit(0) + sys.exit(0) return options @@ -661,12 +688,14 @@ def just_papers(url, channel, username, password): def just_bibtex(url, channel, username, password): + """Retrieve and dump bibtext formated data, unfiltered.""" posts = Mattermost(url, channel, username, password).retrieve() dois = [post.doi for post in posts] print(Bibtex().bib_from_dois(dois)) def main(): + """Run main program.""" opt = parse_args() if opt.dump_posts: just_papers(opt.url, opt.channel, opt.username, opt.password)