~cedric/freshermeat

2e261a7d8b31ade63be3b242ebed327bbbc6ed10 — Cédric Bonhomme 12 days ago 0c2b90d
new: [api v2] added new API using flask-restx.
M freshermeat/web/views/__init__.py => freshermeat/web/views/__init__.py +1 -1
@@ 1,4 1,4 @@
from freshermeat.web.views.api import v1
from freshermeat.web.views.api import v1, v2
from freshermeat.web.views import views, session_mgmt
from freshermeat.web.views.admin import admin_bp
from freshermeat.web.views.user import user_bp

A freshermeat/web/views/api/v2/__init__.py => freshermeat/web/views/api/v2/__init__.py +1 -0
@@ 0,0 1,1 @@
from freshermeat.web.views.api.v2.project import blueprint as blueprint_project

A freshermeat/web/views/api/v2/common.py => freshermeat/web/views/api/v2/common.py +49 -0
@@ 0,0 1,49 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-

# Freshermeat - An open source software directory and release tracker.
# Copyright (C) 2017-2020 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information: https://sr.ht/~cedric/freshermeat
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import logging
from flask import request
from flask_login import current_user
from flask_restless import ProcessingException

from freshermeat.web.views.common import login_user_bundle
from freshermeat.models import User

logger = logging.getLogger(__name__)


def auth_func(func):
    def wrapper(*args, **kwargs):
        if request.authorization:
            user = User.query.filter(User.login == request.authorization.username).first()
            if not user:
                raise ProcessingException("Couldn't authenticate your user", code=401)
            if not user.check_password(request.authorization.password):
                raise ProcessingException("Couldn't authenticate your user", code=401)
            if not user.is_active:
                raise ProcessingException("Couldn't authenticate your user", code=401)
            login_user_bundle(user)
        if not current_user.is_authenticated:
            raise ProcessingException(description="Not authenticated!", code=401)
        return func(*args, **kwargs)
    wrapper.__doc__ = func.__doc__
    wrapper.__name__ = func.__name__
    return wrapper

A freshermeat/web/views/api/v2/project.py => freshermeat/web/views/api/v2/project.py +139 -0
@@ 0,0 1,139 @@
from flask import Blueprint
from flask_login import login_required, current_user
from flask_restx import Api, Resource, fields, reqparse

from freshermeat.bootstrap import db
from freshermeat.models import Project
from freshermeat.web.views.api.v2.common import auth_func


blueprint = Blueprint("api", __name__, url_prefix="/api/v2/projects")
api = Api(
    blueprint,
    title="Freshermeat - API v2",
    version="2.0",
    description="API v2 of Freshermeat.",
    doc="/swagger/",
    # decorators = [auth_func]
    # All API metadatas
)


# Argument Parsing
parser = reqparse.RequestParser()
parser.add_argument("name", type=str, help="Name of the project.")
parser.add_argument("description", type=str, help="Description of the project.")
parser.add_argument(
    "short_description", type=str, help="Short descripton of the project."
)
parser.add_argument("website", type=int, help="Website of the project.")
parser.add_argument("page", type=int, default=1, location="args")
parser.add_argument("per_page", type=int, location="args")


# Response marshalling
project = api.model(
    "Project",
    {
        "id": fields.Integer(readonly=True, description="The project unique identifier"),
        "name": fields.String(
            description="Name of the project.",
        ),
        "description": fields.String(description="The description of the project."),
        "short_description": fields.String(description="The short descripton of the project."),
        "website": fields.String(description="The website of the project."),
        "organization": fields.String(
            attribute=lambda x: x.organization.name if x.organization else None,
            description="The organization related to this project.",
        ),
        "last_updated": fields.DateTime(description="Last update time of the project."),
    },
)

project_list_fields = api.model(
    "ProjectsList",
    {
        "metadata": fields.Raw(
            description="Metada related to the result (number of page, current page, total number of objects)."
        ),
        "data": fields.List(fields.Nested(project), description="List of projects"),
    },
)


@api.route("/")
class ProjectsList(Resource):
    """Shows a list of all projects, and lets you POST to add new projects"""

    @api.doc("list_projects")
    @api.expect(parser)
    @api.marshal_list_with(project_list_fields, skip_none=True)
    def get(self):
        """List all projects"""
        args = parser.parse_args()
        args = {k: v for k, v in args.items() if v is not None}

        page = args.pop("page", 1)
        per_page = args.pop("per_page", 10)

        result = {
            "data": [],
            "metadata": {"total": 0, "count": 0, "page": page, "per_page": per_page,},
        }

        try:
            query = Project.query
            total = query.count()
            projects = query.all()
            count = 0
        except Exception:
            return result, 200
        finally:
            if not projects:
                return result, 200

        result["data"] = projects
        result["metadata"]["total"] = total
        result["metadata"]["count"] = count

        return result, 200

    @api.doc("create_project")
    @api.expect(project)
    @api.marshal_with(project, code=201)
    @auth_func
    def post(self):
        """Create a new project"""
        new_project = Project(**api.payload)
        db.session.add(new_project)
        db.session.commit()
        return new_project, 201


@api.route("/<string:id>")
@api.response(404, "Project not found")
@api.param("id", "The project identifier")
class projectItem(Resource):
    """Show a single project item and lets you delete them"""

    @api.doc("get_project")
    @api.marshal_with(project)
    def get(self, id):
        """Fetch a given resource"""
        return Project.query.filter(Project.id == id).first(), 200

    @api.doc("delete_project")
    @api.response(204, "Project deleted")
    @auth_func
    def delete(self, id):
        """Delete a project given its identifier"""
        # DAO.delete(id)
        return "", 204

    @api.expect(project)
    @api.marshal_with(project)
    @auth_func
    def put(self, id):
        """Update a project given its identifier"""
        # return DAO.update(id, api.payload)
        pass

M poetry.lock => poetry.lock +93 -1
@@ 14,6 14,28 @@ python-editor = ">=0.3"

[[package]]
category = "main"
description = "A library for parsing ISO 8601 strings."
name = "aniso8601"
optional = false
python-versions = "*"
version = "8.0.0"

[[package]]
category = "main"
description = "Classes Without Boilerplate"
name = "attrs"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "19.3.0"

[package.extras]
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
docs = ["sphinx", "zope.interface"]
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]

[[package]]
category = "main"
description = "Fast, simple object-to-object and broadcast signaling"
name = "blinker"
optional = false


@@ 176,6 198,27 @@ sqlalchemy = ">=0.8"

[[package]]
category = "main"
description = "Fully featured framework for fast, easy and documented API development with Flask"
name = "flask-restx"
optional = false
python-versions = "*"
version = "0.2.0"

[package.dependencies]
Flask = ">=0.8"
aniso8601 = ">=0.82"
jsonschema = "*"
pytz = "*"
six = ">=1.3.0"
werkzeug = "*"

[package.extras]
dev = ["blinker", "Faker (2.0.0)", "mock (3.0.5)", "pytest-benchmark (3.2.2)", "pytest-cov (2.7.1)", "pytest-flask (0.15.1)", "pytest-mock (1.10.4)", "pytest-profiling (1.7.0)", "tzlocal", "invoke (1.3.0)", "readme-renderer (24.0)", "twine (1.15.0)", "tox (3.13.2)", "pytest (4.6.5)", "pytest (5.4.1)", "ossaudit", "black (19.10b0)"]
doc = ["alabaster (0.7.12)", "Sphinx (2.1.2)", "sphinx-issues (1.2.0)"]
test = ["blinker", "Faker (2.0.0)", "mock (3.0.5)", "pytest-benchmark (3.2.2)", "pytest-cov (2.7.1)", "pytest-flask (0.15.1)", "pytest-mock (1.10.4)", "pytest-profiling (1.7.0)", "tzlocal", "invoke (1.3.0)", "readme-renderer (24.0)", "twine (1.15.0)", "pytest (4.6.5)", "pytest (5.4.1)", "ossaudit"]

[[package]]
category = "main"
description = "Scripting support for Flask"
name = "flask-script"
optional = false


@@ 253,6 296,24 @@ i18n = ["Babel (>=0.8)"]

[[package]]
category = "main"
description = "An implementation of JSON Schema validation for Python"
name = "jsonschema"
optional = false
python-versions = "*"
version = "3.2.0"

[package.dependencies]
attrs = ">=17.4.0"
pyrsistent = ">=0.14.0"
setuptools = "*"
six = ">=1.11.0"

[package.extras]
format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"]
format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"]

[[package]]
category = "main"
description = "A super-fast templating language that borrows the  best ideas from the existing templating languages."
name = "mako"
optional = false


@@ 336,6 397,7 @@ requests = "^2.23.0"
reference = "5bd4101ad14e14feabb07f5c7bfdc555af7bc699"
type = "git"
url = "https://github.com/cve-search/PyCVESearch.git"

[[package]]
category = "dev"
description = "Python interface to Graphviz's Dot"


@@ 357,6 419,17 @@ version = "2.4.7"

[[package]]
category = "main"
description = "Persistent/Functional/Immutable data structures"
name = "pyrsistent"
optional = false
python-versions = "*"
version = "0.16.0"

[package.dependencies]
six = "*"

[[package]]
category = "main"
description = "Extensions to the standard Python datetime module"
name = "python-dateutil"
optional = false


@@ 537,13 610,21 @@ ipaddress = ["ipaddress"]
locale = ["Babel (>=1.3)"]

[metadata]
content-hash = "8d92209cb8f7f3372c5cc3a92e5b31284951dffdf4a942e7d5107c5ccf9d387a"
content-hash = "7ce8e8228879b9469e845070407dd22489bc804dd1ef34f256aff6b7bdadbed8"
python-versions = "^3.8"

[metadata.files]
alembic = [
    {file = "alembic-1.4.2.tar.gz", hash = "sha256:035ab00497217628bf5d0be82d664d8713ab13d37b630084da8e1f98facf4dbf"},
]
aniso8601 = [
    {file = "aniso8601-8.0.0-py2.py3-none-any.whl", hash = "sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a"},
    {file = "aniso8601-8.0.0.tar.gz", hash = "sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072"},
]
attrs = [
    {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
    {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
]
blinker = [
    {file = "blinker-1.4.tar.gz", hash = "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"},
]


@@ 594,6 675,10 @@ flask-principal = [
flask-restless = [
    {file = "Flask-Restless-0.17.0.tar.gz", hash = "sha256:1de47fe80abd47239c9a1804e0ba5da1d23b9f40cfc26202d16bed37f178c2b6"},
]
flask-restx = [
    {file = "flask-restx-0.2.0.tar.gz", hash = "sha256:ca87a1808333f4ec5a50a5740b44e6cd3879a4b940d559df3996877ec4a2f2a5"},
    {file = "flask_restx-0.2.0-py2.py3-none-any.whl", hash = "sha256:a1653da19ca0b5e5c2ea59bd5f4639a7749e6a9b882f459de1814ed37872253b"},
]
flask-script = [
    {file = "Flask-Script-2.0.6.tar.gz", hash = "sha256:6425963d91054cfcc185807141c7314a9c5ad46325911bd24dcb489bd0161c65"},
]


@@ 621,6 706,10 @@ jinja2 = [
    {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
    {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
]
jsonschema = [
    {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"},
    {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"},
]
mako = [
    {file = "Mako-1.1.2-py2.py3-none-any.whl", hash = "sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9"},
    {file = "Mako-1.1.2.tar.gz", hash = "sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d"},


@@ 724,6 813,9 @@ pyparsing = [
    {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
    {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pyrsistent = [
    {file = "pyrsistent-0.16.0.tar.gz", hash = "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3"},
]
python-dateutil = [
    {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
    {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},

M pyproject.toml => pyproject.toml +1 -0
@@ 26,6 26,7 @@ maya = "^0.6.1"
feedparser = "^5.2.1"
psycopg2-binary = "^2.8.4"
pycvesearch = {git = "https://github.com/cve-search/PyCVESearch.git"}
flask_restx = "^0.2.0"


[tool.poetry.dev-dependencies]

M runserver.py => runserver.py +3 -0
@@ 48,6 48,9 @@ with application.app_context():
    application.register_blueprint(views.api.v1.blueprint_news)
    application.register_blueprint(views.api.v1.blueprint_feed)

    # API v2
    application.register_blueprint(views.api.v2.blueprint_project)


if __name__ == "__main__":
    application.run(host=application.config["HOST"], port=application.config["PORT"])