Skip to content
Snippets Groups Projects
Select Git revision
  • 0e17f9a2a7287876f3d62a016ff638d7fd946fb2
  • master default protected
  • v0.11
  • v0.10
  • v0.9
  • v0.8
  • v0.7
7 results

app.py

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    papersurfer.py 9.91 KiB
    """"Paper surfer - browse papers posted on the mattermost channel.
    
    UI:
    
    [____(filter)______]
    1. paper (open discussion) (open paper)
    2. paper (open discussion) (open paper)
    3. paper (open discussion) (open paper)
    4. paper (open discussion) (open paper)
    
    """
    import subprocess
    from dataclasses import dataclass
    import re
    from functools import partial
    import xml.etree.ElementTree as ET
    import requests
    from mattermostdriver import Driver
    import urwid
    import configargparse
    from pybtex.database import BibliographyData, Entry
    
    URL = "mattermost.cen.uni-hamburg.de"
    CHANNEL = "n5myem9yc7fyzb9am7ym5o41ry"
    
    
    @dataclass
    class PostDTO:
        """"Encapsulate Mattermost Posts."""
        id: str
        message: str
        reporter: str
        doi: str
    
    
    @dataclass
    class PaperDTO:
        """"Encapsulate Mattermost Posts."""
        author: str
        title: str
        journal: str
        year: int
        doi: str
    
    
    class Bibtex:
        def entry(self, author, title, journal, year):
            return BibliographyData({
                f'{author}{year}': Entry('article', [
                    ('author', author),
                    ('title', title),
                    ('journal', journal),
                    ('year', year),
                ])
            }).to_string('bibtex')
    
        def entry_from_doi(self, doi):
            paper = Doi().get_info(doi)
            return self.entry(paper.author, paper.title, paper.journal, paper.year)
    
        def bib_from_dois(self, dois):
            return "\n".join([self.entry_from_doi(doi) for doi in dois])
    
    
    class Doi:
        """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):
            headers = {
                'Accept': 'application/vnd.crossref.unixsd+xml',
            }
            return requests.get(f'http://dx.doi.org/{doi}',
                                headers=headers).content
    
        def parse_doi_xml(self, xml):
            root = ET.fromstring(xml)
            scheme = ".//{http://www.crossref.org/xschema/1.1}"
            author = root.find(f"{scheme}surname").text  # fixme
            title = root.find(f"{scheme}title").text
            journal = root.find(f"{scheme}full_title").text
            year = root.find(f"{scheme}year").text
            doi = root.find(f"{scheme}doi").text
    
            return PaperDTO(author, title, journal, year, doi)
    
        def get_info(self, doi):
            xml = self.load_doi_data(doi)
            return self.parse_doi_xml(xml)
    
        def extract_doi(self, hay):
            """Parse doi from string, or None if not found.
    
            >>> Doi().extract_doi("https://doi.org/10.1093/petrology/egaa077")
            '10.1093/petrology/egaa077'
            """
            pattern = r'\b10\.\d{4,9}/[-._;()/:A-Z0-9]+'
            matches = re.compile(pattern, re.I).search(hay)
            return matches.group() if matches else None
    
    
    class Mattermost:
        """Provide a simplified interaction w/ mattermost api."""
        def __init__(self, username, password):
            self.mattermost = Driver({
                'url': URL,
                'login_id': username,
                'password': password,
                'port': 443
            })
            self.mattermost.login()
            self.reporters = {}
    
        def get_reporter(self, id):
            """Load user from mattermost api and cache."""
            if id not in self.reporters:
                self.reporters[id] = self.mattermost.users.get_user(id)["username"]
    
            return self.reporters[id]
    
        def retrieve_all_messages(self):
            """Retrieve all messages from mattermost, unfiltered for papers."""
            posts = self.mattermost.posts.get_posts_for_channel(CHANNEL)
            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):
            """Filter messages from mattermost to only papers."""
            return [p for p in posts if "doi" in p.message]
    
        def retrieve(self):
            """Retrieve papers from mattermost channel."""
            msgs = self.retrieve_all_messages()
            self.msgs = self.filter_incoming(msgs)
            return self.msgs
    
        def get_filtered(self, needle):
            """Filter posts by needle."""
            return [m for m in self.msgs
                    if needle.lower() in m.message.lower()
                    or needle.lower() in m.reporter.lower()]
    
    
    class PrettyButton(urwid.Button):
        button_left = urwid.Text('[')
        button_right = urwid.Text(']')
    
        def __init__(self, *args, onclick=lambda *x:x, **kwargs):
            super(self.__class__, self).__init__(*args, **kwargs)
            urwid.connect_signal(self, 'click', onclick)
    
        def button(self, label, onclick):
            """Render a pretty button."""
            btn = urwid.Button(label)
            urwid.connect_signal(btn, 'click', onclick)
    
            wrapper = urwid.AttrMap(btn, '', 'highlight')
            padding = urwid.Padding(wrapper, left=4, right=4)
    
            return padding, btn
    
    
    class Papersurfer:
        """Provide UI and interface with mattermost class."""
        def __init__(self, username, password):
            self._screen = urwid.raw_display.Screen()
            self.size = self._screen.get_cols_rows()
            self.filter = ""
    
            palette = [
                ('I say', 'default,bold', 'default', 'bold'),
                ('needle', 'default, bold, underline', 'default', 'bold'),
                ('highlight', 'black', 'dark blue'),
            ]
            ask = urwid.Edit(('I say', u"Filter?\n"))
            exitbutton = PrettyButton(u'Exit', onclick=self.on_exit_clicked)
            self.exportbutton = PrettyButton(u'Export filtered list as bibtex',
                                             onclick=self.on_export_clicked)
            div = urwid.Divider(u'-')
    
            self.mtm = Mattermost(username, password)
    
            body = [self.list_item(paper) for paper in self.mtm.retrieve()]
            self.listcontent = urwid.SimpleFocusListWalker(body)
    
            paperlist = urwid.BoxAdapter(urwid.ListBox(self.listcontent),
                                         self.size[1] - 5)
            pile = urwid.Pile([ask, div, paperlist, div,
                               urwid.Columns([exitbutton, self.exportbutton])])
            top = urwid.Filler(pile, valign='middle')
    
            urwid.connect_signal(ask, 'change', self.onchange)
    
            self.mainloop = urwid.MainLoop(top, palette)
            self.mainloop.run()
    
        def list_item(self, paper, needle=""):
            """Create highlighted text entry."""
            text_items = []
            needle = needle or "ßß"
            msg = f"{paper.message} ({paper.reporter})"
            needles = re.findall(needle, msg, flags=re.IGNORECASE)
            hay = re.split(needle, msg, flags=re.IGNORECASE)
            for i, item in enumerate(hay):
                text_items.append(item)
                if i < len(needles):
                    text_items.append(('needle', needles[i]))
    
            title = urwid.Text(text_items)
            discuss_button = PrettyButton("Open Discussion",
                                           onclick=partial(self.h_open_discussion,
                                                         paper))
            doi_button = PrettyButton("Open DOI",
                                      onclick=partial(self.h_open_doi, paper))
            button_bar = urwid.Columns([discuss_button, doi_button])
            pile = urwid.Pile([title, button_bar, urwid.Divider()])
            return pile
    
        def updscrn(self):
            """"Update (redraw) screen."""
            self.mainloop.draw_screen()
    
        def onchange(self, _, needle):
            """Handle filter change."""
            self.filter = needle
            self.listcontent.clear()
            self.listcontent.extend([self.list_item(paper, needle)
                                     for paper in self.mtm.get_filtered(needle)])
    
        def running_export(self, state):
            btn = self.exportbutton
            label = btn.get_label()
            running_indicator = " (running...)"
            if state:
                btn.set_label(label + running_indicator)
            else:
                btn.set_label(label.replace(running_indicator, ""))
            self.updscrn()
    
        def on_exit_clicked(self, button):
            """Handle exitbutton click and exit."""
            raise urwid.ExitMainLoop()
    
        def on_export_clicked(self, _):
            """Handle exitbutton click and exit."""
            self.running_export(True)
            self.export_to_bibtex()
            self.running_export(False)
    
        def export_to_bibtex(self):
            papers = self.mtm.get_filtered(self.filter)
            dois = [paper.doi for paper in papers]
            string = Bibtex().bib_from_dois(dois)
            with open("export.bib", 'w') as file:
                file.write(string)
    
        def h_open_discussion(self, post, _):
            """Handle click/enter on discussion button."""
            self.open_discussion(post)
    
        def h_open_doi(self, post, _):
            """Handle click/enter on doi button."""
            self.open_doi(post)
    
        def open_discussion(self, post):
            """Open Mattermost post in browser."""
            link = f"https://mattermost.cen.uni-hamburg.de/ifg/pl/{post.id}"
            subprocess.call(["xdg-open", link])
    
        def open_doi(self, post):
            """Open paper page in browser."""
            subprocess.call(["xdg-open", Doi().get_doi_link(post.doi)])
    
    
    def parse_args():
        """Parse command line arguments and config file."""
        parser = configargparse.ArgParser()
        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",
                   is_write_out_config_file_arg=True)
        parser.add('-c', '--my-config', required=False, is_config_file=True,
                   help='config file path', default='papersurfer.conf')
        parser.add('-u', '--username', required=True, help='Mattermost username',
                   default="USERNAME")
        parser.add('-p', '--password', required=True, help='Mattermost password',
                   default="PASSWORD")
        options = parser.parse_args()
        return options.username, options.password
    
    
    if __name__ == "__main__":
        USERNAME, PASSWORD = parse_args()
        Papersurfer(USERNAME, PASSWORD)