<commit_before>"""Document file for Pynuts."""

import os
import datetime
import docutils
import jinja2
import docutils.core
import mimetypes
from urllib import quote, unquote
from flask import (Response, render_template, request, redirect, flash,
                   url_for, jsonify)
from werkzeug.datastructures import Headers
from jinja2 import ChoiceLoader
from weasyprint import HTML
from docutils_html5 import Writer

from .environment import create_environment
from .git import Git, ConflictError

class InvalidId(ValueError):
    """The '/' character is not allowed in document identifiers."""

class MetaDocument(type):
    """Metaclass for document classes."""
    def __init__(cls, name, bases, dict_):
        if cls.document_id_template:
            # TODO: find a better endpoint name than the name of the class
            if not cls.type_name:
                cls.type_name = cls.__name__
            cls._pynuts.documents[cls.type_name] = cls
            cls._pynuts.add_url_rule(
                '/_pynuts/resource/%s/<document_id>/'
                '<version>/<path:filename>' % (
                    cls.type_name),
                endpoint='_pynuts-resource/' + cls.type_name,
                view_func=cls.static_route)
            cls._pynuts.add_url_rule(
                '/_pynuts/update_content', '_pynuts-update_content',
                cls.update_content, methods=('POST',))
            if cls.model_path and not os.path.isabs(cls.model_path):
                cls.model_path = os.path.join(
                    cls._pynuts.root_path, cls.model_path)
            cls.document_id_template = unicode(cls.document_id_template)
            super(MetaDocument, cls).__init__(name, bases, dict_)

class Document(object):
    """This class represents a document object. """
    __metaclass__ = MetaDocument

    #: Jinja Environment
    jinja_environment = None

    #: Docutils settings
    docutils_settings = None

    #: Stylesheet name
    stylesheet = 'style.css'

    #: Template generating the document id
    document_id_template = None

    #: Name identifying the document type, default is the class name
    type_name = None

    #: Model path, absolute or relative to the application root path
    model_path = None

    # Templates
    edit_template = 'edit_document.jinja2'

    def __init__(self, document_id, version=None):
        if '/' in repr(document_id):
            raise InvalidId("The '/' character is not allowed in "
                            "document identifiers.")
        self.document_id = document_id
        self.git = Git(
            self._pynuts.document_repository, branch=self.branch,
            commit=version)
        self.archive_git = Git(
            self._pynuts.document_repository, branch=self.archive_branch)

        self.jinja_environment = create_environment()
        self.jinja_environment.loader = ChoiceLoader((
            self.git.jinja_loader(), self.jinja_environment.loader))
        self.jinja_environment.globals['render_rest'] = \
            self._pynuts.render_rest
        # Take the class attribute
        docutils_settings = dict(self.docutils_settings or {})
        docutils_settings['_pynuts'] = self._pynuts
        # Set an attribute instance to mask the class instance
        self.docutils_settings = docutils_settings
        self.data = None

    @classmethod
    def list_document_ids(cls):
        """Return a list of document ids."""
        base = 'refs/heads/documents/' + quote(cls.type_name.encode('utf8'))
        for doc_id in cls._pynuts.document_repository.refs.keys(base=base):
            yield unquote(doc_id).decode('utf8')

    @classmethod
    def list_documents(cls):
        """Return the whole document list."""
        return (cls(doc_id) for doc_id in cls.list_document_ids())

    @property
    def branch(self):
        """Branch name of the document."""
        return quote((
            'documents/%s/%s' % (self.type_name, self.document_id)
        ).encode('utf8'))

    @property
    def archive_branch(self):
        """Branch name of the document archives."""
        return quote((
            'archives/%s/%s' % (self.type_name, self.document_id)
        ).encode('utf8'))

    @property
    def version(self):
        """Actual git version of the document."""
        return self.git.head.id

    @property
    def datetime(self):
        """Time of the document latest commit as an UTC naive datetime
        object.

        """
        return datetime.datetime.utcfromtimestamp(self.git.head.commit_time)

    @property
    def author(self):
        """Author of the document latest commit."""
        return self.git.head.author.decode('utf-8')

    @property
    def message(self):
        """Message of the document latest commit."""
        return self.git.head.message.decode('utf-8')

    @property
    def history(self):
        """Yield the parent documents."""
        git = Git(self._pynuts.document_repository, branch=self.branch)
        for version in git.history():
            yield type(self)(self.document_id, version=version)

    @property
    def archive_history(self):
        """Yield the parent documents stored as archives."""
        for version in self.archive_git.history():
            yield type(self)(self.document_id, version=version)

    @classmethod
    def from_data(cls, version=None, **kwargs):
        """Create an instance of the class from the given data."""
        doc_id = cls.document_id_template.format(**kwargs)
        instance = cls(doc_id, version=version)
        instance.data = kwargs
        return instance

    def resource_git_url(self, filename):
        """Resource content encoded in base64."""
        return self.git.make_weasyprint_url(filename)

    def resource_http_url(self, filename):
        """Resource URL for the application."""
        return url_for(
            '_pynuts-resource/' + self.type_name,
            document_id=self.document_id,
            filename=filename,
            version=self.version)

    @classmethod
    def static_route(cls, document_id, filename, version):
        """Serve static files for documents.

        :param document_id: id of the document
        :param filename: name of the document
        :param version: version of the document

        """
        mimetype, _ = mimetypes.guess_type(filename)
        return Response(
            cls(document_id, version).git.read(filename), mimetype=mimetype)

    @classmethod
    def generate_rest(cls, part='index.rst.jinja2', resource_type='http',
                      archive=False, version=None, **kwargs):
        return cls.from_data(version=version, **kwargs)._generate_rest(
            part=part, resource_type=resource_type, archive=archive)

    def _generate_rest(self, part='index.rst.jinja2', resource_type='http',
                      archive=False):
        """Generate the ReStructuredText version of the document.

        :param part: part of the document to render
        :param resource_type: external resource type: `http` or `git`
        :param version: version of the document to render
        :param archive: return archive content if `True`

        """
        part = 'index.rst' if archive else part
        if archive:
            return self.git.read(part)
        else:
            template = self.jinja_environment.get_template(part)
            resource = getattr(self, 'resource_%s_url' % resource_type)
            return template.render(
                resource=resource, document=self, **self.data)

    @classmethod
    def generate_html(cls, part='index.rst.jinja2', resource_type='http',
                      archive=False, version=None, **kwargs):
        """Generate the html document from a ReST file or a jinja2 template."""
        return cls.from_data(version=version, **kwargs)._generate_html(
            part=part, resource_type=resource_type, archive=archive)

    def _generate_html(self, part='index.rst.jinja2', resource_type='http',
                      archive=False):
        """Generate the HTML samples of the document.

        The output is a dict corresponding to the different HTML samples as
        generated by Docutils.

        :param part: part of the document to render
        :param resource_type: external resource type: 'http' or 'git'
        :param version: version of the document to render

        .. seealso::
           `Docutils writer publish parts
           <http://docutils.sourceforge.net/docs/api/publisher.html\
           #publish-parts-details>`_

        """
        part = 'index.rst' if archive else part
        source = self._generate_rest(
            part=part, archive=archive, resource_type=resource_type)

        resource = getattr(self, 'resource_%s_url' % resource_type)
        settings = dict(self.docutils_settings)
        settings.setdefault('stylesheet', resource(self.stylesheet))
        parts = docutils.core.publish_parts(
            source=source, writer=Writer(),
            settings_overrides=settings)
        return parts

    @classmethod
    def generate_pdf(cls, part='index.rst.jinja2', version=None, archive=False,
                     **kwargs):
        """Generate the PDF document."""
        return cls.from_data(version=version, **kwargs)._generate_pdf(
            part=part, archive=archive)

    def _generate_pdf(self, part='index.rst.jinja2', version=None,
                      archive=False):
        """Generate the PDF version from the document.

        :param part: part of the document to render.
        :param version: version of the document to render.

        """

        part = 'index.rst' if archive else part
        html = self._generate_html(
            part=part, resource_type='git', archive=archive)['whole']
        return HTML(string=html, encoding='utf8',
                # Work around WeasyPrint bug #813:
                base_url='http://example.net/').write_pdf()

    @classmethod
    def download_pdf(cls, part='index.rst.jinja2', version=None, archive=False,
                     filename=None, **kwargs):
        """Get a HTTP response with PDF document as file in attachment.

        :param part: part of the document to render
        :param version: version of the document to render
        :param filename: attachment filename, expect unicode

        """
        part = 'index.rst' if archive else part
        headers = Headers()
        headers.add(
            'Content-Disposition', 'attachment',
            filename=(filename.encode('utf-8') if filename else None))
        pdf = cls.generate_pdf(
            part=part, version=version, archive=archive, **kwargs)
        return Response(pdf, mimetype='application/pdf', headers=headers)

    @classmethod
    def archive(cls, part='index.rst.jinja2', version=None,
                author_name=None, author_email=None, message=None, **kwargs):
        """Archive the given version of the document.

        :param part: part of the document to archive
        :param version: version of the document to archive
        :param author_name: commit author name
        :param author_email: commit author email
        :param message: commit message

        """
        document = cls.from_data(version=version, **kwargs)
        document.git.write(
            os.path.splitext(part)[0],
            document.generate_rest(part=part, **kwargs).encode('utf-8'))
        git = document.archive_git
        git.tree = document.git.tree
        git.commit(
            author_name or 'Pynuts',
            author_email or 'pynut@pynuts.org',
            message or 'Archive %s' % document.document_id)

    @classmethod
    def update_content(cls):
        """Update the ReST document.
        It is used by the javascript/AJAX save function.
        It gets the request as JSON and update all the parts of the document

        return document's information as JSON.

        'See the save function<>_' for more details.

        """

        contents = request.json['data']
        author_name = request.json['author']
        author_email = request.json['author_email']
        message = request.json['message']

        documents = {}
        for values in contents:
            key = (values['document_type'], values['document_id'])
            if key in documents:
                document = documents[key]
            else:
                cls = cls._pynuts.documents[values['document_type']]
                document = cls(values['document_id'], values['version'])
                documents[key] = document
            document.git.write(values['part'],
                               values['content'].encode('utf-8'))
        for document in documents.values():
            try:
                document.git.commit(
                    author_name or 'Pynuts',
                    author_email or 'pynut@pynuts.org',
                    message or 'Edit %s' % document.document_id)
            except ConflictError:
                return False
        return jsonify(documents=[{
            'document_type': document.type_name,
            'document_id': document.document_id,
            'version': document.version}
            for document in documents.values()])

    @classmethod
    def create(cls, author_name=None, author_email=None, message=None,
               **kwargs):
        """Create the ReST document.

        Return `True` if the document has been created, `False` if the
        document id was already used.

        :param author_name: commit author name
        :param author_email: commit author email
        :param message: commit message

        """
        document = cls.from_data(**kwargs)
        git = document.git
        git.tree = git.store_directory(cls.model_path)
        git.commit(
            author_name or 'Pynuts',
            author_email or 'pynut@pynuts.org',
            message or 'Create %s' % document.document_id)

    @classmethod
    def edit(cls, template, part='index.rst.jinja2', version=None,
             author_name=None, author_email=None, message=None, archive=False,
             redirect_url=None, **kwargs):
        """Edit the document.

        :param template: application template with edition form
        :param part: part of the document to edit
        :param version: version of the document to edit
        :param author_name: commit author name
        :param author_email: commit author email
        :param message: commit message
        :param redirect_url: route to go after saving

        Return ``True`` if the document has been edited, ``False`` if the
        document id was already used.

        """
        if request.method == 'POST':
            document = cls.from_data(
                version=request.form['_old_commit'], **kwargs)
            git = document.archive_git if archive else document.git
            git.write(
                'index.rst' if archive else part,
                request.form['document'].encode('utf-8'))
            try:
                git.commit(
                    author_name or 'Pynuts',
                    author_email or 'pynut@pynuts.org',
                    message or 'Edit %s' % document.document_id)
            except ConflictError:
                flash('A conflict happened.', 'error')
            else:
                flash('The document was saved.', 'ok')
                if redirect_url:
                    return redirect(redirect_url)

        return render_template(
            template, cls=cls, part=part, version=version, archive=archive,
            **kwargs)

    @classmethod
    def view_edit(cls, part='index.rst.jinja2', version=None, archive=False,
                  **kwargs):
        """View the document edition form.

        :param part: part of the document to edit
        :param version: version of the document to edit

        """
        part = 'index.rst' if archive else part
        document = cls.from_data(version=version, **kwargs)
        template = document.jinja_environment.get_template(cls.edit_template)
        # TODO: what if archive==True?
        text = document.git.read(part).decode('utf-8')
        return jinja2.Markup(template.render(
            cls=cls, text=text, old_commit=document.git.head.id, **kwargs))

    @classmethod
    def html(cls, template, part='index.rst.jinja2', version=None,
             archive=False, **kwargs):
        """Render the HTML version of the document.

        :param template: application template including the render
        :param part: part of the document to render
        :param version: version of the document to render

        """
        part = 'index.rst' if archive else part
        return render_template(
            template, cls=cls, part=part, version=version, archive=archive,
            **kwargs)

    @classmethod
    def view_html(cls, part='index.rst.jinja2', version=None, archive=False,
                  html_part='article', **kwargs):
        """View the HTML document ready to include in Jinja templates.

        :param part: part of the document to render
        :param version: version of the document to render

        """
        part = 'index.rst' if archive else part
        return jinja2.Markup(
            cls.generate_html(
                part=part, version=version, archive=archive,
                **kwargs)[html_part].decode('utf-8'))
<commit_msg>Add kwargs in generate_rest (documents)<commit_after>"""Document file for Pynuts."""

import os
import datetime
import docutils
import jinja2
import docutils.core
import mimetypes
from urllib import quote, unquote
from flask import (Response, render_template, request, redirect, flash,
                   url_for, jsonify)
from werkzeug.datastructures import Headers
from jinja2 import ChoiceLoader
from weasyprint import HTML
from docutils_html5 import Writer

from .environment import create_environment
from .git import Git, ConflictError

class InvalidId(ValueError):
    """The '/' character is not allowed in document identifiers."""

class MetaDocument(type):
    """Metaclass for document classes."""
    def __init__(cls, name, bases, dict_):
        if cls.document_id_template:
            # TODO: find a better endpoint name than the name of the class
            if not cls.type_name:
                cls.type_name = cls.__name__
            cls._pynuts.documents[cls.type_name] = cls
            cls._pynuts.add_url_rule(
                '/_pynuts/resource/%s/<document_id>/'
                '<version>/<path:filename>' % (
                    cls.type_name),
                endpoint='_pynuts-resource/' + cls.type_name,
                view_func=cls.static_route)
            cls._pynuts.add_url_rule(
                '/_pynuts/update_content', '_pynuts-update_content',
                cls.update_content, methods=('POST',))
            if cls.model_path and not os.path.isabs(cls.model_path):
                cls.model_path = os.path.join(
                    cls._pynuts.root_path, cls.model_path)
            cls.document_id_template = unicode(cls.document_id_template)
            super(MetaDocument, cls).__init__(name, bases, dict_)

class Document(object):
    """This class represents a document object. """
    __metaclass__ = MetaDocument

    #: Jinja Environment
    jinja_environment = None

    #: Docutils settings
    docutils_settings = None

    #: Stylesheet name
    stylesheet = 'style.css'

    #: Template generating the document id
    document_id_template = None

    #: Name identifying the document type, default is the class name
    type_name = None

    #: Model path, absolute or relative to the application root path
    model_path = None

    # Templates
    edit_template = 'edit_document.jinja2'

    def __init__(self, document_id, version=None):
        if '/' in repr(document_id):
            raise InvalidId("The '/' character is not allowed in "
                            "document identifiers.")
        self.document_id = document_id
        self.git = Git(
            self._pynuts.document_repository, branch=self.branch,
            commit=version)
        self.archive_git = Git(
            self._pynuts.document_repository, branch=self.archive_branch)

        self.jinja_environment = create_environment()
        self.jinja_environment.loader = ChoiceLoader((
            self.git.jinja_loader(), self.jinja_environment.loader))
        self.jinja_environment.globals['render_rest'] = \
            self._pynuts.render_rest
        # Take the class attribute
        docutils_settings = dict(self.docutils_settings or {})
        docutils_settings['_pynuts'] = self._pynuts
        # Set an attribute instance to mask the class instance
        self.docutils_settings = docutils_settings
        self.data = None

    @classmethod
    def list_document_ids(cls):
        """Return a list of document ids."""
        base = 'refs/heads/documents/' + quote(cls.type_name.encode('utf8'))
        for doc_id in cls._pynuts.document_repository.refs.keys(base=base):
            yield unquote(doc_id).decode('utf8')

    @classmethod
    def list_documents(cls):
        """Return the whole document list."""
        return (cls(doc_id) for doc_id in cls.list_document_ids())

    @property
    def branch(self):
        """Branch name of the document."""
        return quote((
            'documents/%s/%s' % (self.type_name, self.document_id)
        ).encode('utf8'))

    @property
    def archive_branch(self):
        """Branch name of the document archives."""
        return quote((
            'archives/%s/%s' % (self.type_name, self.document_id)
        ).encode('utf8'))

    @property
    def version(self):
        """Actual git version of the document."""
        return self.git.head.id

    @property
    def datetime(self):
        """Time of the document latest commit as an UTC naive datetime
        object.

        """
        return datetime.datetime.utcfromtimestamp(self.git.head.commit_time)

    @property
    def author(self):
        """Author of the document latest commit."""
        return self.git.head.author.decode('utf-8')

    @property
    def message(self):
        """Message of the document latest commit."""
        return self.git.head.message.decode('utf-8')

    @property
    def history(self):
        """Yield the parent documents."""
        git = Git(self._pynuts.document_repository, branch=self.branch)
        for version in git.history():
            yield type(self)(self.document_id, version=version)

    @property
    def archive_history(self):
        """Yield the parent documents stored as archives."""
        for version in self.archive_git.history():
            yield type(self)(self.document_id, version=version)

    @classmethod
    def from_data(cls, version=None, **kwargs):
        """Create an instance of the class from the given data."""
        doc_id = cls.document_id_template.format(**kwargs)
        instance = cls(doc_id, version=version)
        instance.data = kwargs
        return instance

    def resource_git_url(self, filename):
        """Resource content encoded in base64."""
        return self.git.make_weasyprint_url(filename)

    def resource_http_url(self, filename):
        """Resource URL for the application."""
        return url_for(
            '_pynuts-resource/' + self.type_name,
            document_id=self.document_id,
            filename=filename,
            version=self.version)

    @classmethod
    def static_route(cls, document_id, filename, version):
        """Serve static files for documents.

        :param document_id: id of the document
        :param filename: name of the document
        :param version: version of the document

        """
        mimetype, _ = mimetypes.guess_type(filename)
        return Response(
            cls(document_id, version).git.read(filename), mimetype=mimetype)

    @classmethod
    def generate_rest(cls, part='index.rst.jinja2', resource_type='http',
                      archive=False, version=None, **kwargs):
        return cls.from_data(version=version, **kwargs)._generate_rest(
            part=part, resource_type=resource_type, archive=archive, **kwargs)

    def _generate_rest(self, part='index.rst.jinja2', resource_type='http',
                      archive=False, **kwargs):
        """Generate the ReStructuredText version of the document.

        :param part: part of the document to render
        :param resource_type: external resource type: `http` or `git`
        :param version: version of the document to render
        :param archive: return archive content if `True`

        """
        part = 'index.rst' if archive else part
        if archive:
            return self.git.read(part)
        else:
            template = self.jinja_environment.get_template(part)
            resource = getattr(self, 'resource_%s_url' % resource_type)
            return template.render(
                resource=resource, document=self, **self.data)

    @classmethod
    def generate_html(cls, part='index.rst.jinja2', resource_type='http',
                      archive=False, version=None, **kwargs):
        """Generate the html document from a ReST file or a jinja2 template."""
        return cls.from_data(version=version, **kwargs)._generate_html(
            part=part, resource_type=resource_type, archive=archive)

    def _generate_html(self, part='index.rst.jinja2', resource_type='http',
                      archive=False):
        """Generate the HTML samples of the document.

        The output is a dict corresponding to the different HTML samples as
        generated by Docutils.

        :param part: part of the document to render
        :param resource_type: external resource type: 'http' or 'git'
        :param version: version of the document to render

        .. seealso::
           `Docutils writer publish parts
           <http://docutils.sourceforge.net/docs/api/publisher.html\
           #publish-parts-details>`_

        """
        part = 'index.rst' if archive else part
        source = self._generate_rest(
            part=part, archive=archive, resource_type=resource_type)

        resource = getattr(self, 'resource_%s_url' % resource_type)
        settings = dict(self.docutils_settings)
        settings.setdefault('stylesheet', resource(self.stylesheet))
        parts = docutils.core.publish_parts(
            source=source, writer=Writer(),
            settings_overrides=settings)
        return parts

    @classmethod
    def generate_pdf(cls, part='index.rst.jinja2', version=None, archive=False,
                     **kwargs):
        """Generate the PDF document."""
        return cls.from_data(version=version, **kwargs)._generate_pdf(
            part=part, archive=archive)

    def _generate_pdf(self, part='index.rst.jinja2', version=None,
                      archive=False):
        """Generate the PDF version from the document.

        :param part: part of the document to render.
        :param version: version of the document to render.

        """

        part = 'index.rst' if archive else part
        html = self._generate_html(
            part=part, resource_type='git', archive=archive)['whole']
        return HTML(string=html, encoding='utf8',
                # Work around WeasyPrint bug #813:
                base_url='http://example.net/').write_pdf()

    @classmethod
    def download_pdf(cls, part='index.rst.jinja2', version=None, archive=False,
                     filename=None, **kwargs):
        """Get a HTTP response with PDF document as file in attachment.

        :param part: part of the document to render
        :param version: version of the document to render
        :param filename: attachment filename, expect unicode

        """
        part = 'index.rst' if archive else part
        headers = Headers()
        headers.add(
            'Content-Disposition', 'attachment',
            filename=(filename.encode('utf-8') if filename else None))
        pdf = cls.generate_pdf(
            part=part, version=version, archive=archive, **kwargs)
        return Response(pdf, mimetype='application/pdf', headers=headers)

    @classmethod
    def archive(cls, part='index.rst.jinja2', version=None,
                author_name=None, author_email=None, message=None, **kwargs):
        """Archive the given version of the document.

        :param part: part of the document to archive
        :param version: version of the document to archive
        :param author_name: commit author name
        :param author_email: commit author email
        :param message: commit message

        """
        document = cls.from_data(version=version, **kwargs)
        document.git.write(
            os.path.splitext(part)[0],
            document.generate_rest(part=part, **kwargs).encode('utf-8'))
        git = document.archive_git
        git.tree = document.git.tree
        git.commit(
            author_name or 'Pynuts',
            author_email or 'pynut@pynuts.org',
            message or 'Archive %s' % document.document_id)

    @classmethod
    def update_content(cls):
        """Update the ReST document.
        It is used by the javascript/AJAX save function.
        It gets the request as JSON and update all the parts of the document

        return document's information as JSON.

        'See the save function<>_' for more details.

        """

        contents = request.json['data']
        author_name = request.json['author']
        author_email = request.json['author_email']
        message = request.json['message']

        documents = {}
        for values in contents:
            key = (values['document_type'], values['document_id'])
            if key in documents:
                document = documents[key]
            else:
                cls = cls._pynuts.documents[values['document_type']]
                document = cls(values['document_id'], values['version'])
                documents[key] = document
            document.git.write(values['part'],
                               values['content'].encode('utf-8'))
        for document in documents.values():
            try:
                document.git.commit(
                    author_name or 'Pynuts',
                    author_email or 'pynut@pynuts.org',
                    message or 'Edit %s' % document.document_id)
            except ConflictError:
                return False
        return jsonify(documents=[{
            'document_type': document.type_name,
            'document_id': document.document_id,
            'version': document.version}
            for document in documents.values()])

    @classmethod
    def create(cls, author_name=None, author_email=None, message=None,
               **kwargs):
        """Create the ReST document.

        Return `True` if the document has been created, `False` if the
        document id was already used.

        :param author_name: commit author name
        :param author_email: commit author email
        :param message: commit message

        """
        document = cls.from_data(**kwargs)
        git = document.git
        git.tree = git.store_directory(cls.model_path)
        git.commit(
            author_name or 'Pynuts',
            author_email or 'pynut@pynuts.org',
            message or 'Create %s' % document.document_id)

    @classmethod
    def edit(cls, template, part='index.rst.jinja2', version=None,
             author_name=None, author_email=None, message=None, archive=False,
             redirect_url=None, **kwargs):
        """Edit the document.

        :param template: application template with edition form
        :param part: part of the document to edit
        :param version: version of the document to edit
        :param author_name: commit author name
        :param author_email: commit author email
        :param message: commit message
        :param redirect_url: route to go after saving

        Return ``True`` if the document has been edited, ``False`` if the
        document id was already used.

        """
        if request.method == 'POST':
            document = cls.from_data(
                version=request.form['_old_commit'], **kwargs)
            git = document.archive_git if archive else document.git
            git.write(
                'index.rst' if archive else part,
                request.form['document'].encode('utf-8'))
            try:
                git.commit(
                    author_name or 'Pynuts',
                    author_email or 'pynut@pynuts.org',
                    message or 'Edit %s' % document.document_id)
            except ConflictError:
                flash('A conflict happened.', 'error')
            else:
                flash('The document was saved.', 'ok')
                if redirect_url:
                    return redirect(redirect_url)

        return render_template(
            template, cls=cls, part=part, version=version, archive=archive,
            **kwargs)

    @classmethod
    def view_edit(cls, part='index.rst.jinja2', version=None, archive=False,
                  **kwargs):
        """View the document edition form.

        :param part: part of the document to edit
        :param version: version of the document to edit

        """
        part = 'index.rst' if archive else part
        document = cls.from_data(version=version, **kwargs)
        template = document.jinja_environment.get_template(cls.edit_template)
        # TODO: what if archive==True?
        text = document.git.read(part).decode('utf-8')
        return jinja2.Markup(template.render(
            cls=cls, text=text, old_commit=document.git.head.id, **kwargs))

    @classmethod
    def html(cls, template, part='index.rst.jinja2', version=None,
             archive=False, **kwargs):
        """Render the HTML version of the document.

        :param template: application template including the render
        :param part: part of the document to render
        :param version: version of the document to render

        """
        part = 'index.rst' if archive else part
        return render_template(
            template, cls=cls, part=part, version=version, archive=archive,
            **kwargs)

    @classmethod
    def view_html(cls, part='index.rst.jinja2', version=None, archive=False,
                  html_part='article', **kwargs):
        """View the HTML document ready to include in Jinja templates.

        :param part: part of the document to render
        :param version: version of the document to render

        """
        part = 'index.rst' if archive else part
        return jinja2.Markup(
            cls.generate_html(
                part=part, version=version, archive=archive,
                **kwargs)[html_part].decode('utf-8'))
<|endoftext|>


<commit_before>"""
PyPika is divided into a couple of modules, primarily the ``queries`` and ``terms`` modules.

pypika.queries
--------------

This is where the ``Query`` class can be found which is the core class in PyPika.  Also, other top level classes such
as ``Table`` can be found here.  ``Query`` is a container that holds all of the ``Term`` types together and also
serializes the builder to a string.

pypika.terms
------------

This module contains the classes which represent individual parts of queries that extend the ``Term`` base class.

pypika.functions
----------------

Wrappers for common SQL functions are stored in this package.

pypika.enums
------------

Enumerated values are kept in this package which are used as options for Queries and Terms.

pypika.utils
------------

This contains all of the utility classes such as exceptions and decorators.

"""
# noinspection PyUnresolvedReferences
from .dialects import (
    Dialects,
    MSSQLQuery,
    MySQLQuery,
    OracleQuery,
    PostgreSQLQuery,
    RedshiftQuery,
    VerticaQuery,
)
# noinspection PyUnresolvedReferences
from .enums import (
    DatePart,
    JoinType,
    Order,
)
# noinspection PyUnresolvedReferences
from .queries import (
    Query,
    Table,
    Tuple,
    make_tables as Tables,
)
# noinspection PyUnresolvedReferences
from .terms import (
    Case,
    Field,
    Interval,
    Not,
    Rollup,
)
# noinspection PyUnresolvedReferences
from .utils import (
    CaseException,
    GroupingException,
    JoinException,
    RollupException,
    UnionException,
)

__author__ = "Timothy Heys"
__email__ = "theys@kayak.com"
__version__ = "0.15.2"
<commit_msg>Bumped version to 0.15.3<commit_after>"""
PyPika is divided into a couple of modules, primarily the ``queries`` and ``terms`` modules.

pypika.queries
--------------

This is where the ``Query`` class can be found which is the core class in PyPika.  Also, other top level classes such
as ``Table`` can be found here.  ``Query`` is a container that holds all of the ``Term`` types together and also
serializes the builder to a string.

pypika.terms
------------

This module contains the classes which represent individual parts of queries that extend the ``Term`` base class.

pypika.functions
----------------

Wrappers for common SQL functions are stored in this package.

pypika.enums
------------

Enumerated values are kept in this package which are used as options for Queries and Terms.

pypika.utils
------------

This contains all of the utility classes such as exceptions and decorators.

"""
# noinspection PyUnresolvedReferences
from .dialects import (
    Dialects,
    MSSQLQuery,
    MySQLQuery,
    OracleQuery,
    PostgreSQLQuery,
    RedshiftQuery,
    VerticaQuery,
)
# noinspection PyUnresolvedReferences
from .enums import (
    DatePart,
    JoinType,
    Order,
)
# noinspection PyUnresolvedReferences
from .queries import (
    Query,
    Table,
    Tuple,
    make_tables as Tables,
)
# noinspection PyUnresolvedReferences
from .terms import (
    Case,
    Field,
    Interval,
    Not,
    Rollup,
)
# noinspection PyUnresolvedReferences
from .utils import (
    CaseException,
    GroupingException,
    JoinException,
    RollupException,
    UnionException,
)

__author__ = "Timothy Heys"
__email__ = "theys@kayak.com"
__version__ = "0.15.3"
<|endoftext|>


<commit_before># coding: utf8
"""
PyPika is divided into a couple of modules, primarily the ``queries`` and ``terms`` modules.

pypika.queries
--------------

This is where the ``Query`` class can be found which is the core class in PyPika.  Also, other top level classes such
as ``Table`` can be found here.  ``Query`` is a container that holds all of the ``Term`` types together and also
serializes the builder to a string.

pypika.terms
------------

This module contains the classes which represent individual parts of queries that extend the ``Term`` base class.

pypika.functions
----------------

Wrappers for common SQL functions are stored in this package.

pypika.enums
------------

Enumerated values are kept in this package which are used as options for Queries and Terms.

pypika.utils
------------

This contains all of the utility classes such as exceptions and decorators.

"""
from .enums import Order, JoinType, DatePart
from .queries import Query, Table, make_tables as Tables
from .terms import Field, Case, Interval, Rollup
from .utils import JoinException, GroupingException, CaseException, UnionException, RollupException

__author__ = "Timothy Heys"
__email__ = "theys@kayak.com"
__version__ = "0.1.3"<commit_msg>Bumped versino to 0.1.4<commit_after># coding: utf8
"""
PyPika is divided into a couple of modules, primarily the ``queries`` and ``terms`` modules.

pypika.queries
--------------

This is where the ``Query`` class can be found which is the core class in PyPika.  Also, other top level classes such
as ``Table`` can be found here.  ``Query`` is a container that holds all of the ``Term`` types together and also
serializes the builder to a string.

pypika.terms
------------

This module contains the classes which represent individual parts of queries that extend the ``Term`` base class.

pypika.functions
----------------

Wrappers for common SQL functions are stored in this package.

pypika.enums
------------

Enumerated values are kept in this package which are used as options for Queries and Terms.

pypika.utils
------------

This contains all of the utility classes such as exceptions and decorators.

"""
from .enums import Order, JoinType, DatePart
from .queries import Query, Table, make_tables as Tables
from .terms import Field, Case, Interval, Rollup
from .utils import JoinException, GroupingException, CaseException, UnionException, RollupException

__author__ = "Timothy Heys"
__email__ = "theys@kayak.com"
__version__ = "0.1.4"<|endoftext|>


<commit_before>"""
PyPika is divided into a couple of modules, primarily the ``queries`` and ``terms`` modules.

pypika.queries
--------------

This is where the ``Query`` class can be found which is the core class in PyPika.  Also, other top level classes such
as ``Table`` can be found here.  ``Query`` is a container that holds all of the ``Term`` types together and also
serializes the builder to a string.

pypika.terms
------------

This module contains the classes which represent individual parts of queries that extend the ``Term`` base class.

pypika.functions
----------------

Wrappers for common SQL functions are stored in this package.

pypika.enums
------------

Enumerated values are kept in this package which are used as options for Queries and Terms.

pypika.utils
------------

This contains all of the utility classes such as exceptions and decorators.

"""
# noinspection PyUnresolvedReferences
from .dialects import (
    ClickHouseQuery,
    Dialects,
    MSSQLQuery,
    MySQLQuery,
    OracleQuery,
    PostgreSQLQuery,
    RedshiftQuery,
    SQLLiteQuery,
    VerticaQuery,
)
# noinspection PyUnresolvedReferences
from .enums import (
    DatePart,
    JoinType,
    Order,
)
# noinspection PyUnresolvedReferences
from .queries import (
    AliasedQuery,
    Query,
    Schema,
    Table,
    make_tables as Tables,
)
# noinspection PyUnresolvedReferences
from .terms import (
    Array,
    Bracket,
    Case,
    Criterion,
    EmptyCriterion,
    Field,
    Interval,
    Not,
    NullValue,
    Parameter,
    Rollup,
    Tuple,
)
# noinspection PyUnresolvedReferences
from .utils import (
    CaseException,
    GroupingException,
    JoinException,
    QueryException,
    RollupException,
    UnionException,
)

__author__ = 'Timothy Heys'
__email__ = 'theys@kayak.com'
__version__ = '0.27.0'
<commit_msg>bumped version to 0.27.1<commit_after>"""
PyPika is divided into a couple of modules, primarily the ``queries`` and ``terms`` modules.

pypika.queries
--------------

This is where the ``Query`` class can be found which is the core class in PyPika.  Also, other top level classes such
as ``Table`` can be found here.  ``Query`` is a container that holds all of the ``Term`` types together and also
serializes the builder to a string.

pypika.terms
------------

This module contains the classes which represent individual parts of queries that extend the ``Term`` base class.

pypika.functions
----------------

Wrappers for common SQL functions are stored in this package.

pypika.enums
------------

Enumerated values are kept in this package which are used as options for Queries and Terms.

pypika.utils
------------

This contains all of the utility classes such as exceptions and decorators.

"""
# noinspection PyUnresolvedReferences
from .dialects import (
    ClickHouseQuery,
    Dialects,
    MSSQLQuery,
    MySQLQuery,
    OracleQuery,
    PostgreSQLQuery,
    RedshiftQuery,
    SQLLiteQuery,
    VerticaQuery,
)
# noinspection PyUnresolvedReferences
from .enums import (
    DatePart,
    JoinType,
    Order,
)
# noinspection PyUnresolvedReferences
from .queries import (
    AliasedQuery,
    Query,
    Schema,
    Table,
    make_tables as Tables,
)
# noinspection PyUnresolvedReferences
from .terms import (
    Array,
    Bracket,
    Case,
    Criterion,
    EmptyCriterion,
    Field,
    Interval,
    Not,
    NullValue,
    Parameter,
    Rollup,
    Tuple,
)
# noinspection PyUnresolvedReferences
from .utils import (
    CaseException,
    GroupingException,
    JoinException,
    QueryException,
    RollupException,
    UnionException,
)

__author__ = 'Timothy Heys'
__email__ = 'theys@kayak.com'
__version__ = '0.27.1'
<|endoftext|>


<commit_before>#!/usr/bin/python3
# encoding=utf-8

import argparse
import requests
import urllib
import re

# parse the console params
parser = argparse.ArgumentParser()
parser.add_argument('domain', help='a domain like baidu.com | a ipv4 addr', type=str)
parser.add_argument('-v', help='show request detail info', action='store_true')
parser.add_argument('-s', help='show your public network GEO info', action='store_true')
args = parser.parse_args()

print('Query IP/Host is : %s\n' % args.domain)

# build remote url
ip = args.domain            # query ip addr
host = 'ip.chinaz.com'
url = 'http://ip.chinaz.com/?IP=skinsharp.cn'
payload = {
    'ip': ip,
}
headers = {
    'Host': host,
    'User-Agent': 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': "en-US,en;q=0.5",
    'Accept-Encoding': "gzip, deflate",
    'Referer': url,
    'Connection': 'keep-alive',
    'Content-Type': 'application/x-www-form-urlencoded',
}

# http request via requests python lib
r = requests.post(url, headers=headers, data=payload, timeout=1)
if args.v:
    print('Request     URL: %s' % url)
    print('Request  header:')
    print(headers)
    print('Request payload: %s' % urllib.parse.urlencode(payload))
    print('Response status: %d' % r.status_code)
    print('Response header:')
    print(r.headers)            # dict type
    # print(r.text)             # ascii text type
    print()

# filter result from the html content
re_code = re.compile(r'(.*?)')
fields = re.findall(re_code, r.text)

# stdout the data
i = 0
cache = []
for zone in fields:
    cache.append(zone[1].strip())
    i = i+1
    if i == 4:
        print('%s \t%-15s \t%s \t%s' % (cache[0], cache[1], cache[2], cache[3]))
        i = 0
        del cache[:]
del cache
del i

# filter the local ip of public network
if args.s:
    re_code = re.compile(r'您来自：(.*)?所在区域：(.*)?<a href')
    fields = re.findall(re_code, r.text)
    for i in fields:
        print('\n你的公网IP： %s, IP物理地址：%s' % (i[0].strip(), i[1]))

del fields
<commit_msg>modify the python3 env<commit_after>#!/usr/bin/env python3
# encoding=utf-8

import argparse
import requests
import urllib
import re

# parse the console params
parser = argparse.ArgumentParser()
parser.add_argument('domain', help='a domain like baidu.com | a ipv4 addr', type=str)
parser.add_argument('-v', help='show request detail info', action='store_true')
parser.add_argument('-s', help='show your public network GEO info', action='store_true')
args = parser.parse_args()

print('Query IP/Host is : %s\n' % args.domain)

# build remote url
ip = args.domain            # query ip addr
host = 'ip.chinaz.com'
url = 'http://ip.chinaz.com/?IP=skinsharp.cn'
payload = {
    'ip': ip,
}
headers = {
    'Host': host,
    'User-Agent': 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': "en-US,en;q=0.5",
    'Accept-Encoding': "gzip, deflate",
    'Referer': url,
    'Connection': 'keep-alive',
    'Content-Type': 'application/x-www-form-urlencoded',
}

# http request via requests python lib
r = requests.post(url, headers=headers, data=payload, timeout=1)
if args.v:
    print('Request     URL: %s' % url)
    print('Request  header:')
    print(headers)
    print('Request payload: %s' % urllib.parse.urlencode(payload))
    print('Response status: %d' % r.status_code)
    print('Response header:')
    print(r.headers)            # dict type
    # print(r.text)             # ascii text type
    print()

# filter result from the html content
re_code = re.compile(r'(.*?)')
fields = re.findall(re_code, r.text)

# stdout the data
i = 0
cache = []
for zone in fields:
    cache.append(zone[1].strip())
    i = i+1
    if i == 4:
        print('%s \t%-15s \t%s \t%s' % (cache[0], cache[1], cache[2], cache[3]))
        i = 0
        del cache[:]
del cache
del i

# filter the local ip of public network
if args.s:
    re_code = re.compile(r'您来自：(.*)?所在区域：(.*)?<a href')
    fields = re.findall(re_code, r.text)
    for i in fields:
        print('\n你的公网IP： %s, IP物理地址：%s' % (i[0].strip(), i[1]))

del fields
<|endoftext|>


<commit_before>#  -*- Mode: python; coding:utf-8; indent-tabs-mode: nil -*- */
#
#  This file is part of systemd.
#
#  Copyright 2012 David Strauss <david@davidstrauss.net>
#  Copyright 2012 Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
#  Copyright 2012 Marti Raudsepp <marti@juffo.org>
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.
#
#  systemd is distributed in the hope that it will be useful, but
#  WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public License
#  along with systemd; If not, see <http://www.gnu.org/licenses/>.

import sys as _sys
import datetime as _datetime
import functools as _functools
import uuid as _uuid
import traceback as _traceback
import os as _os
from os import SEEK_SET, SEEK_CUR, SEEK_END
import logging as _logging
if _sys.version_info >= (3,):
    from collections import ChainMap
from syslog import (LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR,
                    LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG)
from ._journal import sendv, stream_fd
from ._reader import (_Journal, NOP, APPEND, INVALIDATE,
                      LOCAL_ONLY, RUNTIME_ONLY, SYSTEM_ONLY)
from . import id128 as _id128

_MONOTONIC_CONVERTER = lambda x: _datetime.timedelta(microseconds=float(x))
_REALTIME_CONVERTER = lambda x: _datetime.datetime.fromtimestamp(float(x)/1E6)
DEFAULT_CONVERTERS = {
    'MESSAGE_ID': _uuid.UUID,
    '_MACHINE_ID': _uuid.UUID,
    '_BOOT_ID': _uuid.UUID,
    'PRIORITY': int,
    'LEADER': int,
    'SESSION_ID': int,
    'USERSPACE_USEC': int,
    'INITRD_USEC': int,
    'KERNEL_USEC': int,
    '_UID': int,
    '_GID': int,
    '_PID': int,
    'SYSLOG_FACILITY': int,
    'SYSLOG_PID': int,
    '_AUDIT_SESSION': int,
    '_AUDIT_LOGINUID': int,
    '_SYSTEMD_SESSION': int,
    '_SYSTEMD_OWNER_UID': int,
    'CODE_LINE': int,
    'ERRNO': int,
    'EXIT_STATUS': int,
    '_SOURCE_REALTIME_TIMESTAMP': _REALTIME_CONVERTER,
    '__REALTIME_TIMESTAMP': _REALTIME_CONVERTER,
    '_SOURCE_MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER,
    '__MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER,
    'COREDUMP': bytes,
    'COREDUMP_PID': int,
    'COREDUMP_UID': int,
    'COREDUMP_GID': int,
    'COREDUMP_SESSION': int,
    'COREDUMP_SIGNAL': int,
    'COREDUMP_TIMESTAMP': _REALTIME_CONVERTER,
}

if _sys.version_info >= (3,):
    _convert_unicode = _functools.partial(str, encoding='utf-8')
else:
    _convert_unicode = _functools.partial(unicode, encoding='utf-8')

class Journal(_Journal):
    def __init__(self, converters=None, flags=LOCAL_ONLY, path=None):
        """Creates instance of Journal, which allows filtering and
        return of journal entries.
        Argument `converters` is a dictionary which updates the
        DEFAULT_CONVERTERS to convert journal field values.
        Argument `flags` sets open flags of the journal, which can be one
        of, or ORed combination of constants: LOCAL_ONLY (default) opens
        journal on local machine only; RUNTIME_ONLY opens only
        volatile journal files; and SYSTEM_ONLY opens only
        journal files of system services and the kernel.
        Argument `path` is the directory of journal files. Note that
        currently flags are ignored when `path` is present as they are
        currently not relevant.
        """
        super(Journal, self).__init__(flags, path)
        if _sys.version_info >= (3,3):
            self.converters = ChainMap()
            if converters is not None:
                self.converters.maps.append(converters)
            self.converters.maps.append(DEFAULT_CONVERTERS)
        else:
            self.converters = DEFAULT_CONVERTERS.copy()
            if converters is not None:
                self.converters.update(converters)

    def _convert_field(self, key, value):
        """ Convert value based on callable from self.converters
        based of field/key"""
        try:
            result = self.converters[key](value)
        except:
            # Default conversion in unicode
            try:
                result = _convert_unicode(value)
            except:
                # Leave in default bytes
                result = value
        return result

    def _convert_entry(self, entry):
        """ Convert entire journal entry utilising _covert_field"""
        result = {}
        for key, value in entry.items():
            if isinstance(value, list):
                result[key] = [self._convert_field(key, val) for val in value]
            else:
                result[key] = self._convert_field(key, value)
        return result

    def add_match(self, *args, **kwargs):
        """Add one or more matches to the filter journal log entries.
        All matches of different field are combined in a logical AND,
        and matches of the smae field are automatically combined in a
        logical OR.
        Matches can be passed as strings of form "field=value", or
        keyword arguments field="value"."""
        args = list(args)
        args.extend(_make_line(key, val) for key, val in kwargs.items())
        for arg in args:
            super(Journal, self).add_match(arg)

    def get_next(self, skip=1):
        """Return dictionary of the next log entry. Optional skip value
        will return the `skip`th log entry.
        Returned will be journal entry dictionary processed with
        converters."""
        return self._convert_entry(
            super(Journal, self).get_next(skip))

    def query_unique(self, field):
        """Returns a set of unique values in journal for given `field`,
        processed with converters.
        Note this does not respect any journal matches."""
        return set(self._convert_field(field, value)
            for value in super(Journal, self).query_unique(field))

    def seek_realtime(self, realtime):
        """Seek to nearest matching journal entry to `realtime`.
        Argument `realtime` can must be either an integer unix timestamp
        or datetime.datetime instance."""
        if isinstance(realtime, _datetime.datetime):
            realtime = float(realtime.strftime("%s.%f"))
        return super(Journal, self).seek_realtime(realtime)

    def seek_monotonic(self, monotonic, bootid=None):
        """Seek to nearest matching journal entry to `monotonic`.
        Argument `monotonic` is a timestamp from boot in either seconds
        or a datetime.timedelta instance.
        Argument `bootid` is a string or UUID representing which boot the
        monotonic time is reference to. Defaults to current bootid."""
        if isinstance(monotonic, _datetime.timedelta):
            monotonic = monotonic.totalseconds()
        if isinstance(bootid, _uuid.UUID):
            bootid = bootid.get_hex()
        return super(Journal, self).seek_monotonic(monotonic, bootid)

    def log_level(self, level):
        """Sets maximum log `level` by setting matches for PRIORITY."""
        if 0 <= level <= 7:
            for i in range(level+1):
                self.add_match(PRIORITY="%s" % i)
        else:
            raise ValueError("Log level must be 0 <= level <= 7")

    def this_boot(self, bootid=None):
        """Add match for _BOOT_ID equal to current boot ID or the specified boot ID.

        If specified, bootid should be either a UUID or a 32 digit hex number.

        Equivalent to add_match(_BOOT_ID='bootid').
        """
        if bootid is None:
            bootid = _id128.get_boot().hex
        else:
            bootid = getattr(bootid, 'hex', bootid)
        self.add_match(_BOOT_ID=bootid)

    def this_machine(self, machineid=None):
        """Add match for _MACHINE_ID equal to the ID of this machine.

        If specified, machineid should be either a UUID or a 32 digit hex number.

        Equivalent to add_match(_MACHINE_ID='machineid').
        """
        if machineid is None:
            machineid = _id128.get_machine().hex
        else:
            machineid = getattr(machineid, 'hex', machineid)
        self.add_match(_MACHINE_ID=machineid)

def _make_line(field, value):
        if isinstance(value, bytes):
                return field.encode('utf-8') + b'=' + value
        else:
                return field + '=' + value

def send(MESSAGE, MESSAGE_ID=None,
         CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None,
         **kwargs):
        r"""Send a message to the journal.

        >>> journal.send('Hello world')
        >>> journal.send('Hello, again, world', FIELD2='Greetings!')
        >>> journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef')

        Value of the MESSAGE argument will be used for the MESSAGE=
        field. MESSAGE must be a string and will be sent as UTF-8 to
        the journal.

        MESSAGE_ID can be given to uniquely identify the type of
        message. It must be a string or a uuid.UUID object.

        CODE_LINE, CODE_FILE, and CODE_FUNC can be specified to
        identify the caller. Unless at least on of the three is given,
        values are extracted from the stack frame of the caller of
        send(). CODE_FILE and CODE_FUNC must be strings, CODE_LINE
        must be an integer.

        Additional fields for the journal entry can only be specified
        as keyword arguments. The payload can be either a string or
        bytes. A string will be sent as UTF-8, and bytes will be sent
        as-is to the journal.

        Other useful fields include PRIORITY, SYSLOG_FACILITY,
        SYSLOG_IDENTIFIER, SYSLOG_PID.
        """

        args = ['MESSAGE=' + MESSAGE]

        if MESSAGE_ID is not None:
                id = getattr(MESSAGE_ID, 'hex', MESSAGE_ID)
                args.append('MESSAGE_ID=' + id)

        if CODE_LINE == CODE_FILE == CODE_FUNC == None:
                CODE_FILE, CODE_LINE, CODE_FUNC = \
                        _traceback.extract_stack(limit=2)[0][:3]
        if CODE_FILE is not None:
                args.append('CODE_FILE=' + CODE_FILE)
        if CODE_LINE is not None:
                args.append('CODE_LINE={:d}'.format(CODE_LINE))
        if CODE_FUNC is not None:
                args.append('CODE_FUNC=' + CODE_FUNC)

        args.extend(_make_line(key, val) for key, val in kwargs.items())
        return sendv(*args)

def stream(identifier, priority=LOG_DEBUG, level_prefix=False):
        r"""Return a file object wrapping a stream to journal.

        Log messages written to this file as simple newline sepearted
        text strings are written to the journal.

        The file will be line buffered, so messages are actually sent
        after a newline character is written.

        >>> stream = journal.stream('myapp')
        >>> stream
        <open file '<fdopen>', mode 'w' at 0x...>
        >>> stream.write('message...\n')

        will produce the following message in the journal::

          PRIORITY=7
          SYSLOG_IDENTIFIER=myapp
          MESSAGE=message...

        Using the interface with print might be more convinient:

        >>> from __future__ import print_function
        >>> print('message...', file=stream)

        priority is the syslog priority, one of `LOG_EMERG`,
        `LOG_ALERT`, `LOG_CRIT`, `LOG_ERR`, `LOG_WARNING`,
        `LOG_NOTICE`, `LOG_INFO`, `LOG_DEBUG`.

        level_prefix is a boolean. If true, kernel-style log priority
        level prefixes (such as '<1>') are interpreted. See
        sd-daemon(3) for more information.
        """

        fd = stream_fd(identifier, priority, level_prefix)
        return _os.fdopen(fd, 'w', 1)

class JournalHandler(_logging.Handler):
        """Journal handler class for the Python logging framework.

        Please see the Python logging module documentation for an
        overview: http://docs.python.org/library/logging.html.

        To create a custom logger whose messages go only to journal:

        >>> log = logging.getLogger('custom_logger_name')
        >>> log.propagate = False
        >>> log.addHandler(journal.JournalHandler())
        >>> log.warn("Some message: %s", detail)

        Note that by default, message levels `INFO` and `DEBUG` are
        ignored by the logging framework. To enable those log levels:

        >>> log.setLevel(logging.DEBUG)

        To attach journal MESSAGE_ID, an extra field is supported:

        >>> import uuid
        >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF')
        >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid})

        To redirect all logging messages to journal regardless of where
        they come from, attach it to the root logger:

        >>> logging.root.addHandler(journal.JournalHandler())

        For more complex configurations when using `dictConfig` or
        `fileConfig`, specify `systemd.journal.JournalHandler` as the
        handler class.  Only standard handler configuration options
        are supported: `level`, `formatter`, `filters`.

        The following journal fields will be sent:
        `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`,
        `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call),
        `MESSAGE_ID` (optional, see above).
        """

        def emit(self, record):
                """Write record as journal event.

                MESSAGE is taken from the message provided by the
                user, and PRIORITY, LOGGER, THREAD_NAME,
                CODE_{FILE,LINE,FUNC} fields are appended
                automatically. In addition, record.MESSAGE_ID will be
                used if present.
                """
                try:
                        msg = self.format(record)
                        pri = self.mapPriority(record.levelno)
                        mid = getattr(record, 'MESSAGE_ID', None)
                        send(msg,
                             MESSAGE_ID=mid,
                             PRIORITY=format(pri),
                             LOGGER=record.name,
                             THREAD_NAME=record.threadName,
                             CODE_FILE=record.pathname,
                             CODE_LINE=record.lineno,
                             CODE_FUNC=record.funcName)
                except Exception:
                        self.handleError(record)

        @staticmethod
        def mapPriority(levelno):
                """Map logging levels to journald priorities.

                Since Python log level numbers are "sparse", we have
                to map numbers in between the standard levels too.
                """
                if levelno <= _logging.DEBUG:
                        return LOG_DEBUG
                elif levelno <= _logging.INFO:
                        return LOG_INFO
                elif levelno <= _logging.WARNING:
                        return LOG_WARNING
                elif levelno <= _logging.ERROR:
                        return LOG_ERR
                elif levelno <= _logging.CRITICAL:
                        return LOG_CRIT
                else:
                        return LOG_ALERT
<commit_msg>systemd-python: add Journal method to add MESSAGE_ID match<commit_after>#  -*- Mode: python; coding:utf-8; indent-tabs-mode: nil -*- */
#
#  This file is part of systemd.
#
#  Copyright 2012 David Strauss <david@davidstrauss.net>
#  Copyright 2012 Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
#  Copyright 2012 Marti Raudsepp <marti@juffo.org>
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.
#
#  systemd is distributed in the hope that it will be useful, but
#  WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public License
#  along with systemd; If not, see <http://www.gnu.org/licenses/>.

import sys as _sys
import datetime as _datetime
import functools as _functools
import uuid as _uuid
import traceback as _traceback
import os as _os
from os import SEEK_SET, SEEK_CUR, SEEK_END
import logging as _logging
if _sys.version_info >= (3,):
    from collections import ChainMap
from syslog import (LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR,
                    LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG)
from ._journal import sendv, stream_fd
from ._reader import (_Journal, NOP, APPEND, INVALIDATE,
                      LOCAL_ONLY, RUNTIME_ONLY, SYSTEM_ONLY)
from . import id128 as _id128

_MONOTONIC_CONVERTER = lambda x: _datetime.timedelta(microseconds=float(x))
_REALTIME_CONVERTER = lambda x: _datetime.datetime.fromtimestamp(float(x)/1E6)
DEFAULT_CONVERTERS = {
    'MESSAGE_ID': _uuid.UUID,
    '_MACHINE_ID': _uuid.UUID,
    '_BOOT_ID': _uuid.UUID,
    'PRIORITY': int,
    'LEADER': int,
    'SESSION_ID': int,
    'USERSPACE_USEC': int,
    'INITRD_USEC': int,
    'KERNEL_USEC': int,
    '_UID': int,
    '_GID': int,
    '_PID': int,
    'SYSLOG_FACILITY': int,
    'SYSLOG_PID': int,
    '_AUDIT_SESSION': int,
    '_AUDIT_LOGINUID': int,
    '_SYSTEMD_SESSION': int,
    '_SYSTEMD_OWNER_UID': int,
    'CODE_LINE': int,
    'ERRNO': int,
    'EXIT_STATUS': int,
    '_SOURCE_REALTIME_TIMESTAMP': _REALTIME_CONVERTER,
    '__REALTIME_TIMESTAMP': _REALTIME_CONVERTER,
    '_SOURCE_MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER,
    '__MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER,
    'COREDUMP': bytes,
    'COREDUMP_PID': int,
    'COREDUMP_UID': int,
    'COREDUMP_GID': int,
    'COREDUMP_SESSION': int,
    'COREDUMP_SIGNAL': int,
    'COREDUMP_TIMESTAMP': _REALTIME_CONVERTER,
}

if _sys.version_info >= (3,):
    _convert_unicode = _functools.partial(str, encoding='utf-8')
else:
    _convert_unicode = _functools.partial(unicode, encoding='utf-8')

class Journal(_Journal):
    def __init__(self, converters=None, flags=LOCAL_ONLY, path=None):
        """Creates instance of Journal, which allows filtering and
        return of journal entries.
        Argument `converters` is a dictionary which updates the
        DEFAULT_CONVERTERS to convert journal field values.
        Argument `flags` sets open flags of the journal, which can be one
        of, or ORed combination of constants: LOCAL_ONLY (default) opens
        journal on local machine only; RUNTIME_ONLY opens only
        volatile journal files; and SYSTEM_ONLY opens only
        journal files of system services and the kernel.
        Argument `path` is the directory of journal files. Note that
        currently flags are ignored when `path` is present as they are
        currently not relevant.
        """
        super(Journal, self).__init__(flags, path)
        if _sys.version_info >= (3,3):
            self.converters = ChainMap()
            if converters is not None:
                self.converters.maps.append(converters)
            self.converters.maps.append(DEFAULT_CONVERTERS)
        else:
            self.converters = DEFAULT_CONVERTERS.copy()
            if converters is not None:
                self.converters.update(converters)

    def _convert_field(self, key, value):
        """ Convert value based on callable from self.converters
        based of field/key"""
        try:
            result = self.converters[key](value)
        except:
            # Default conversion in unicode
            try:
                result = _convert_unicode(value)
            except:
                # Leave in default bytes
                result = value
        return result

    def _convert_entry(self, entry):
        """ Convert entire journal entry utilising _covert_field"""
        result = {}
        for key, value in entry.items():
            if isinstance(value, list):
                result[key] = [self._convert_field(key, val) for val in value]
            else:
                result[key] = self._convert_field(key, value)
        return result

    def add_match(self, *args, **kwargs):
        """Add one or more matches to the filter journal log entries.
        All matches of different field are combined in a logical AND,
        and matches of the smae field are automatically combined in a
        logical OR.
        Matches can be passed as strings of form "field=value", or
        keyword arguments field="value"."""
        args = list(args)
        args.extend(_make_line(key, val) for key, val in kwargs.items())
        for arg in args:
            super(Journal, self).add_match(arg)

    def get_next(self, skip=1):
        """Return dictionary of the next log entry. Optional skip value
        will return the `skip`th log entry.
        Returned will be journal entry dictionary processed with
        converters."""
        return self._convert_entry(
            super(Journal, self).get_next(skip))

    def query_unique(self, field):
        """Returns a set of unique values in journal for given `field`,
        processed with converters.
        Note this does not respect any journal matches."""
        return set(self._convert_field(field, value)
            for value in super(Journal, self).query_unique(field))

    def seek_realtime(self, realtime):
        """Seek to nearest matching journal entry to `realtime`.
        Argument `realtime` can must be either an integer unix timestamp
        or datetime.datetime instance."""
        if isinstance(realtime, _datetime.datetime):
            realtime = float(realtime.strftime("%s.%f"))
        return super(Journal, self).seek_realtime(realtime)

    def seek_monotonic(self, monotonic, bootid=None):
        """Seek to nearest matching journal entry to `monotonic`.
        Argument `monotonic` is a timestamp from boot in either seconds
        or a datetime.timedelta instance.
        Argument `bootid` is a string or UUID representing which boot the
        monotonic time is reference to. Defaults to current bootid."""
        if isinstance(monotonic, _datetime.timedelta):
            monotonic = monotonic.totalseconds()
        if isinstance(bootid, _uuid.UUID):
            bootid = bootid.get_hex()
        return super(Journal, self).seek_monotonic(monotonic, bootid)

    def log_level(self, level):
        """Sets maximum log `level` by setting matches for PRIORITY."""
        if 0 <= level <= 7:
            for i in range(level+1):
                self.add_match(PRIORITY="%s" % i)
        else:
            raise ValueError("Log level must be 0 <= level <= 7")

    def messageid_match(self, messageid):
        """Sets match filter for log entries for specified `messageid`.
        `messageid` can be string or UUID instance.
        Standard message IDs can be found in systemd.id128
        Equivalent to add_match(MESSAGE_ID=`messageid`)."""
        if isinstance(messageid, _uuid.UUID):
            messageid = messageid.get_hex()
        self.add_match(MESSAGE_ID=messageid)

    def this_boot(self, bootid=None):
        """Add match for _BOOT_ID equal to current boot ID or the specified boot ID.

        If specified, bootid should be either a UUID or a 32 digit hex number.

        Equivalent to add_match(_BOOT_ID='bootid').
        """
        if bootid is None:
            bootid = _id128.get_boot().hex
        else:
            bootid = getattr(bootid, 'hex', bootid)
        self.add_match(_BOOT_ID=bootid)

    def this_machine(self, machineid=None):
        """Add match for _MACHINE_ID equal to the ID of this machine.

        If specified, machineid should be either a UUID or a 32 digit hex number.

        Equivalent to add_match(_MACHINE_ID='machineid').
        """
        if machineid is None:
            machineid = _id128.get_machine().hex
        else:
            machineid = getattr(machineid, 'hex', machineid)
        self.add_match(_MACHINE_ID=machineid)

def _make_line(field, value):
        if isinstance(value, bytes):
                return field.encode('utf-8') + b'=' + value
        else:
                return field + '=' + value

def send(MESSAGE, MESSAGE_ID=None,
         CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None,
         **kwargs):
        r"""Send a message to the journal.

        >>> journal.send('Hello world')
        >>> journal.send('Hello, again, world', FIELD2='Greetings!')
        >>> journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef')

        Value of the MESSAGE argument will be used for the MESSAGE=
        field. MESSAGE must be a string and will be sent as UTF-8 to
        the journal.

        MESSAGE_ID can be given to uniquely identify the type of
        message. It must be a string or a uuid.UUID object.

        CODE_LINE, CODE_FILE, and CODE_FUNC can be specified to
        identify the caller. Unless at least on of the three is given,
        values are extracted from the stack frame of the caller of
        send(). CODE_FILE and CODE_FUNC must be strings, CODE_LINE
        must be an integer.

        Additional fields for the journal entry can only be specified
        as keyword arguments. The payload can be either a string or
        bytes. A string will be sent as UTF-8, and bytes will be sent
        as-is to the journal.

        Other useful fields include PRIORITY, SYSLOG_FACILITY,
        SYSLOG_IDENTIFIER, SYSLOG_PID.
        """

        args = ['MESSAGE=' + MESSAGE]

        if MESSAGE_ID is not None:
                id = getattr(MESSAGE_ID, 'hex', MESSAGE_ID)
                args.append('MESSAGE_ID=' + id)

        if CODE_LINE == CODE_FILE == CODE_FUNC == None:
                CODE_FILE, CODE_LINE, CODE_FUNC = \
                        _traceback.extract_stack(limit=2)[0][:3]
        if CODE_FILE is not None:
                args.append('CODE_FILE=' + CODE_FILE)
        if CODE_LINE is not None:
                args.append('CODE_LINE={:d}'.format(CODE_LINE))
        if CODE_FUNC is not None:
                args.append('CODE_FUNC=' + CODE_FUNC)

        args.extend(_make_line(key, val) for key, val in kwargs.items())
        return sendv(*args)

def stream(identifier, priority=LOG_DEBUG, level_prefix=False):
        r"""Return a file object wrapping a stream to journal.

        Log messages written to this file as simple newline sepearted
        text strings are written to the journal.

        The file will be line buffered, so messages are actually sent
        after a newline character is written.

        >>> stream = journal.stream('myapp')
        >>> stream
        <open file '<fdopen>', mode 'w' at 0x...>
        >>> stream.write('message...\n')

        will produce the following message in the journal::

          PRIORITY=7
          SYSLOG_IDENTIFIER=myapp
          MESSAGE=message...

        Using the interface with print might be more convinient:

        >>> from __future__ import print_function
        >>> print('message...', file=stream)

        priority is the syslog priority, one of `LOG_EMERG`,
        `LOG_ALERT`, `LOG_CRIT`, `LOG_ERR`, `LOG_WARNING`,
        `LOG_NOTICE`, `LOG_INFO`, `LOG_DEBUG`.

        level_prefix is a boolean. If true, kernel-style log priority
        level prefixes (such as '<1>') are interpreted. See
        sd-daemon(3) for more information.
        """

        fd = stream_fd(identifier, priority, level_prefix)
        return _os.fdopen(fd, 'w', 1)

class JournalHandler(_logging.Handler):
        """Journal handler class for the Python logging framework.

        Please see the Python logging module documentation for an
        overview: http://docs.python.org/library/logging.html.

        To create a custom logger whose messages go only to journal:

        >>> log = logging.getLogger('custom_logger_name')
        >>> log.propagate = False
        >>> log.addHandler(journal.JournalHandler())
        >>> log.warn("Some message: %s", detail)

        Note that by default, message levels `INFO` and `DEBUG` are
        ignored by the logging framework. To enable those log levels:

        >>> log.setLevel(logging.DEBUG)

        To attach journal MESSAGE_ID, an extra field is supported:

        >>> import uuid
        >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF')
        >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid})

        To redirect all logging messages to journal regardless of where
        they come from, attach it to the root logger:

        >>> logging.root.addHandler(journal.JournalHandler())

        For more complex configurations when using `dictConfig` or
        `fileConfig`, specify `systemd.journal.JournalHandler` as the
        handler class.  Only standard handler configuration options
        are supported: `level`, `formatter`, `filters`.

        The following journal fields will be sent:
        `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`,
        `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call),
        `MESSAGE_ID` (optional, see above).
        """

        def emit(self, record):
                """Write record as journal event.

                MESSAGE is taken from the message provided by the
                user, and PRIORITY, LOGGER, THREAD_NAME,
                CODE_{FILE,LINE,FUNC} fields are appended
                automatically. In addition, record.MESSAGE_ID will be
                used if present.
                """
                try:
                        msg = self.format(record)
                        pri = self.mapPriority(record.levelno)
                        mid = getattr(record, 'MESSAGE_ID', None)
                        send(msg,
                             MESSAGE_ID=mid,
                             PRIORITY=format(pri),
                             LOGGER=record.name,
                             THREAD_NAME=record.threadName,
                             CODE_FILE=record.pathname,
                             CODE_LINE=record.lineno,
                             CODE_FUNC=record.funcName)
                except Exception:
                        self.handleError(record)

        @staticmethod
        def mapPriority(levelno):
                """Map logging levels to journald priorities.

                Since Python log level numbers are "sparse", we have
                to map numbers in between the standard levels too.
                """
                if levelno <= _logging.DEBUG:
                        return LOG_DEBUG
                elif levelno <= _logging.INFO:
                        return LOG_INFO
                elif levelno <= _logging.WARNING:
                        return LOG_WARNING
                elif levelno <= _logging.ERROR:
                        return LOG_ERR
                elif levelno <= _logging.CRITICAL:
                        return LOG_CRIT
                else:
                        return LOG_ALERT
<|endoftext|>


<commit_before># Generated by Django 2.2.20 on 2021-05-02 13:17

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ('dojo', '0096_engagement_type'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='engagement',
            name='eng_type',
        ),
        migrations.DeleteModel(
            name='Engagement_Type',
        ),
    ]
<commit_msg>Delete 0097_engagement_type.py<commit_after><|endoftext|>


<commit_before>#!/usr/bin/env python
# -*- coding: utf-8 -*-
# <bitbar.title>Package Manager</bitbar.title>
# <bitbar.version>v1.1</bitbar.version>
# <bitbar.author>Kevin Deldycke</bitbar.author>
# <bitbar.author.github>kdeldycke</bitbar.author.github>
# <bitbar.desc>List package updates available from Homebrew, Cask and Pip. Allows individual or full upgrades (if available).</bitbar.desc>
# <bitbar.dependencies>python,homebrew,cask,pip</bitbar.dependencies>
# <bitbar.image>https://i.imgur.com/oXL2Nyn.png</bitbar.image>
# <bitbar.abouturl>https://github.com/kdeldycke/dotfiles/blob/master/dotfiles-osx/.bitbar/package_manager.7h.py</bitbar.abouturl>

from __future__ import print_function, unicode_literals

from subprocess import Popen, PIPE
import sys
import json
import os
from operator import methodcaller
import re

# TODO: add cleanup commands.

class PackageManager(object):
    """ Generic class for a package manager. """

    def __init__(self):
        # List all available updates and their versions.
        self.updates = []

    @property
    def name(self):
        """ Return package manager's common name. Defaults based on class name.
        """
        return self.__class__.__name__

    @property
    def active(self):
        """ Is the package manager available on the system?

        Returns True is the main CLI exists and is executable.
        """
        return os.path.isfile(self.cli) and os.access(self.cli, os.X_OK)

    @staticmethod
    def run(*args):
        """ Run a shell command, and exits right away on error. """
        output, error = Popen(args, stdout=PIPE).communicate()
        if error:
            print("Error | color=red")
            sys.exit(error)
        return output

    def sync(self):
        """ Fetch latest versions of installed packages.

        Returns a list of dict with package name, current installed version and
        latest upgradeable version.
        """
        raise NotImplementedError

    @staticmethod
    def bitbar_cli_format(full_cli):
        """ Format a bash-runnable full-CLI with parameters into bitbar schema.
        """
        cmd, params = full_cli.strip().split(' ', 1)
        bitbar_cli = "bash={}".format(cmd)
        for index, param in enumerate(params.split(' ')):
            bitbar_cli += " param{}={}".format(index + 1, param)
        return bitbar_cli

    def update_cli(self, package_name):
        """ Return a bitbar-compatible full-CLI to update a package. """
        raise NotImplementedError

    def update_all_cli(self):
        """ Return a bitbar-compatible full-CLI to update all packages. """
        raise NotImplementedError

class Homebrew(PackageManager):

    cli = '/usr/local/bin/brew'

    def sync(self):
        """ Fetch latest Homebrew formulas. """
        self.run(self.cli, 'update')

        # List available updates.
        output = self.run(self.cli, 'outdated', '--json=v1')

        for pkg_info in json.loads(output):
            self.updates.append({
                'name': pkg_info['name'],
                # Only keeps the highest installed version.
                'installed_version': max(pkg_info['installed_versions']),
                'latest_version': pkg_info['current_version']})

    def update_cli(self, package_name=None):
        cmd = "{} upgrade --cleanup".format(self.cli)
        if package_name:
            cmd += " {}".format(package_name)
        return self.bitbar_cli_format(cmd)

    def update_all_cli(self):
        return self.update_cli()

class Cask(Homebrew):

    def sync(self):
        """ Fetch latest formulas and their metadata. """
        # No need to update formulas if Homebrew is synced first.

        # List installed packages.
        output = self.run(self.cli, 'cask', 'list', '--versions')

        for installed_pkg in output.strip().split('\n'):
            name, versions = installed_pkg.split(' ', 1)

            # `brew cask list` is broken. Use heuristics to guess the currently
            # installed version.
            # See: https://github.com/caskroom/homebrew-cask/issues/14058
            versions = sorted([
                v.strip() for v in versions.split(',') if v.strip()])
            if len(versions) > 1 and 'latest' in versions:
                versions.remove('latest')
            version = versions[-1] if versions else '?'

            # Look closer to the package to guess its state.
            output = self.run(self.cli, 'cask', 'info', name)

            # Package is up-to-date.
            if output.find('Not installed') == -1:
                continue

            latest_version = output.split('\n')[0].split(' ')[1]

            self.updates.append({
                'name': name,
                'installed_version': version,
                'latest_version': latest_version})

    def update_cli(self, package_name):
        return self.bitbar_cli_format(
            "{} cask install {}".format(self.cli, package_name))

    def update_all_cli(self):
        """ Cask has no way to update all outdated packages. """
        return

class Pip(PackageManager):

    cli = '/usr/local/bin/pip'

    def sync(self):
        """ List outdated packages and their metadata. """
        output = self.run(self.cli, 'list', '--outdated')

        regexp = re.compile(r'(\S+) \((.*)\) - Latest: (\S+)')

        for outdated_pkg in output.strip().split('\n'):
            name, installed_info, latest_version = regexp.match(
                outdated_pkg).groups()

            # Extract current non-standard location if found.
            installed_info = installed_info.split(',', 1)
            version = installed_info[0]
            special_location = " ({})".format(
                installed_info[1].strip()) if len(installed_info) > 1 else ''

            self.updates.append({
                'name': name + special_location,
                'installed_version': version,
                'latest_version': latest_version})

    def update_cli(self, package_name):
        return self.bitbar_cli_format(
            "{} install --upgrade {}".format(self.cli, package_name))

    def update_all_cli(self):
        """ Pip doesn't support full upgrade yet. """
        return

def print_menu():
    """ Print menu structure using BitBar's plugin API.

    See: https://github.com/matryer/bitbar#plugin-api
    """
    # Instantiate all available package manager.
    managers = [k() for k in [Homebrew, Cask, Pip]]

    # Filters-out inactive managers.
    managers = [m for m in managers if m.active]

    # Sync all managers.
    map(methodcaller('sync'), managers)

    # Print menu bar icon with number of available updates.
    total_updates = sum([len(m.updates) for m in managers])
    print(("↑{} | dropdown=false".format(total_updates)).encode('utf-8'))

    # Print a full detailed section for each manager.
    for manager in managers:
        print("---")

        print("{} {} package{}".format(
            len(manager.updates),
            manager.name,
            's' if len(manager.updates) > 1 else ''))

        if manager.update_all_cli() and manager.updates:
            print("Upgrade all | {} terminal=false refresh=true".format(
                manager.update_all_cli()))

        for pkg_info in manager.updates:
            print((
                "{name} {installed_version} → {latest_version} | "
                "{cli} terminal=false refresh=true".format(
                    cli=manager.update_cli(pkg_info['name']),
                    **pkg_info)).encode('utf-8'))

print_menu()
<commit_msg>Fix parsing of pip output when there's no outdated packages.<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-
# <bitbar.title>Package Manager</bitbar.title>
# <bitbar.version>v1.1</bitbar.version>
# <bitbar.author>Kevin Deldycke</bitbar.author>
# <bitbar.author.github>kdeldycke</bitbar.author.github>
# <bitbar.desc>List package updates available from Homebrew, Cask and Pip. Allows individual or full upgrades (if available).</bitbar.desc>
# <bitbar.dependencies>python,homebrew,cask,pip</bitbar.dependencies>
# <bitbar.image>https://i.imgur.com/oXL2Nyn.png</bitbar.image>
# <bitbar.abouturl>https://github.com/kdeldycke/dotfiles/blob/master/dotfiles-osx/.bitbar/package_manager.7h.py</bitbar.abouturl>

from __future__ import print_function, unicode_literals

from subprocess import Popen, PIPE
import sys
import json
import os
from operator import methodcaller
import re

# TODO: add cleanup commands.

class PackageManager(object):
    """ Generic class for a package manager. """

    def __init__(self):
        # List all available updates and their versions.
        self.updates = []

    @property
    def name(self):
        """ Return package manager's common name. Defaults based on class name.
        """
        return self.__class__.__name__

    @property
    def active(self):
        """ Is the package manager available on the system?

        Returns True is the main CLI exists and is executable.
        """
        return os.path.isfile(self.cli) and os.access(self.cli, os.X_OK)

    @staticmethod
    def run(*args):
        """ Run a shell command, and exits right away on error. """
        output, error = Popen(args, stdout=PIPE).communicate()
        if error:
            print("Error | color=red")
            sys.exit(error)
        return output

    def sync(self):
        """ Fetch latest versions of installed packages.

        Returns a list of dict with package name, current installed version and
        latest upgradeable version.
        """
        raise NotImplementedError

    @staticmethod
    def bitbar_cli_format(full_cli):
        """ Format a bash-runnable full-CLI with parameters into bitbar schema.
        """
        cmd, params = full_cli.strip().split(' ', 1)
        bitbar_cli = "bash={}".format(cmd)
        for index, param in enumerate(params.split(' ')):
            bitbar_cli += " param{}={}".format(index + 1, param)
        return bitbar_cli

    def update_cli(self, package_name):
        """ Return a bitbar-compatible full-CLI to update a package. """
        raise NotImplementedError

    def update_all_cli(self):
        """ Return a bitbar-compatible full-CLI to update all packages. """
        raise NotImplementedError

class Homebrew(PackageManager):

    cli = '/usr/local/bin/brew'

    def sync(self):
        """ Fetch latest Homebrew formulas. """
        self.run(self.cli, 'update')

        # List available updates.
        output = self.run(self.cli, 'outdated', '--json=v1')

        for pkg_info in json.loads(output):
            self.updates.append({
                'name': pkg_info['name'],
                # Only keeps the highest installed version.
                'installed_version': max(pkg_info['installed_versions']),
                'latest_version': pkg_info['current_version']})

    def update_cli(self, package_name=None):
        cmd = "{} upgrade --cleanup".format(self.cli)
        if package_name:
            cmd += " {}".format(package_name)
        return self.bitbar_cli_format(cmd)

    def update_all_cli(self):
        return self.update_cli()

class Cask(Homebrew):

    def sync(self):
        """ Fetch latest formulas and their metadata. """
        # No need to update formulas if Homebrew is synced first.

        # List installed packages.
        output = self.run(self.cli, 'cask', 'list', '--versions')

        for installed_pkg in output.strip().split('\n'):
            name, versions = installed_pkg.split(' ', 1)

            # `brew cask list` is broken. Use heuristics to guess the currently
            # installed version.
            # See: https://github.com/caskroom/homebrew-cask/issues/14058
            versions = sorted([
                v.strip() for v in versions.split(',') if v.strip()])
            if len(versions) > 1 and 'latest' in versions:
                versions.remove('latest')
            version = versions[-1] if versions else '?'

            # Look closer to the package to guess its state.
            output = self.run(self.cli, 'cask', 'info', name)

            # Package is up-to-date.
            if output.find('Not installed') == -1:
                continue

            latest_version = output.split('\n')[0].split(' ')[1]

            self.updates.append({
                'name': name,
                'installed_version': version,
                'latest_version': latest_version})

    def update_cli(self, package_name):
        return self.bitbar_cli_format(
            "{} cask install {}".format(self.cli, package_name))

    def update_all_cli(self):
        """ Cask has no way to update all outdated packages. """
        return

class Pip(PackageManager):

    cli = '/usr/local/bin/pip'

    def sync(self):
        """ List outdated packages and their metadata. """
        output = self.run(self.cli, 'list', '--outdated').strip()
        if not output:
            return

        regexp = re.compile(r'(\S+) \((.*)\) - Latest: (\S+)')
        for outdated_pkg in output.split('\n'):
            name, installed_info, latest_version = regexp.match(
                outdated_pkg).groups()

            # Extract current non-standard location if found.
            installed_info = installed_info.split(',', 1)
            version = installed_info[0]
            special_location = " ({})".format(
                installed_info[1].strip()) if len(installed_info) > 1 else ''

            self.updates.append({
                'name': name + special_location,
                'installed_version': version,
                'latest_version': latest_version})

    def update_cli(self, package_name):
        return self.bitbar_cli_format(
            "{} install --upgrade {}".format(self.cli, package_name))

    def update_all_cli(self):
        """ Pip doesn't support full upgrade yet. """
        return

def print_menu():
    """ Print menu structure using BitBar's plugin API.

    See: https://github.com/matryer/bitbar#plugin-api
    """
    # Instantiate all available package manager.
    managers = [k() for k in [Homebrew, Cask, Pip]]

    # Filters-out inactive managers.
    managers = [m for m in managers if m.active]

    # Sync all managers.
    map(methodcaller('sync'), managers)

    # Print menu bar icon with number of available updates.
    total_updates = sum([len(m.updates) for m in managers])
    print(("↑{} | dropdown=false".format(total_updates)).encode('utf-8'))

    # Print a full detailed section for each manager.
    for manager in managers:
        print("---")

        print("{} {} package{}".format(
            len(manager.updates),
            manager.name,
            's' if len(manager.updates) > 1 else ''))

        if manager.update_all_cli() and manager.updates:
            print("Upgrade all | {} terminal=false refresh=true".format(
                manager.update_all_cli()))

        for pkg_info in manager.updates:
            print((
                "{name} {installed_version} → {latest_version} | "
                "{cli} terminal=false refresh=true".format(
                    cli=manager.update_cli(pkg_info['name']),
                    **pkg_info)).encode('utf-8'))

print_menu()
<|endoftext|>