Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add caching module and refactor code #69

Merged
merged 6 commits into from
Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: Build
on: [push]
on:
push:
schedule:
# runs a new build every saturday
- cron: 0 0 * * 6

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion flask_minify/about.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.35"
__version__ = "0.36"
__doc__ = "Flask extension to minify html, css, js and less."
__license__ = "MIT"
__author__ = "Mohamed Feddad"
Expand Down
44 changes: 44 additions & 0 deletions flask_minify/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from flask_minify.utils import get_optimized_hashing


class MemoryCache:
def __init__(self, store_key_getter=None, limit=0):
self.store_key_getter = store_key_getter
self.limit = limit
self._cache = {}
self.hashing = get_optimized_hashing()

@property
def store(self):
if self.store_key_getter:
return self._cache.setdefault(self.store_key_getter(), {})

return self._cache

@property
def limit_exceeded(self):
return len(self.store) >= self.limit

def __getitem__(self, key):
return self.store.get(key)

def __setitem__(self, key, value):
if self.limit_exceeded:
self.store.popitem()

self.store.update({key: value})

def get_or_set(self, key, getter):
if self.limit == 0:
return getter()

hashed_key = self.hashing(key.encode("utf-8")).hexdigest()

if not self[hashed_key]:
self[hashed_key] = getter()

return self[hashed_key]

def clear(self):
del self._cache
self._cache = {}
28 changes: 11 additions & 17 deletions flask_minify/decorators.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from functools import wraps

from flask_minify.cache import MemoryCache
from flask_minify.parsers import Parser
from flask_minify.utils import get_optimized_hashing


def minify(
html=False,
js=False,
cssless=False,
cache=True,
caching_limit=2,
fail_safe=True,
parsers={},
):
Expand All @@ -24,6 +25,8 @@ def minify(
enable minifying CSS/LESS content.
cache: bool
enable caching minifed response.
caching_limit: int
to limit the number of minified response variations.
failsafe: bool
silence encountered exceptions.
parsers: dict
Expand All @@ -33,30 +36,21 @@ def minify(
-------
String of minified HTML content.
"""
hashing = get_optimized_hashing()
caching = MemoryCache(caching_limit if cache else 0)
parser = Parser(parsers, fail_safe)
parser.update_runtime_options(html, js, cssless)

def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
text = function(*args, **kwargs)
key = None
cache_key, cached = function.__dict__.get("minify", (None, None))
should_minify = isinstance(text, str) and any([html, js, cssless])
content = function(*args, **kwargs)
should_minify = isinstance(content, str) and any([html, js, cssless])
get_minified = lambda: parser.minify(content, "html")

if should_minify:
if cache:
key = hashing(text).hexdigest()
if not should_minify:
return content

if cache_key != key or not cache:
text = parser.minify(text, "html")

if cache:
function.__dict__["minify"] = (key, text)

should_return_cached = cache_key == key and cache and should_minify
return cached if should_return_cached else text
return caching.get_or_set(content, get_minified)

return wrapper

Expand Down
115 changes: 45 additions & 70 deletions flask_minify/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

from flask import _app_ctx_stack, request

from flask_minify.cache import MemoryCache
from flask_minify.parsers import Parser
from flask_minify.utils import get_optimized_hashing, is_cssless, is_html, is_js
from flask_minify.utils import does_content_type_match


class Minify:
Expand Down Expand Up @@ -84,41 +85,30 @@ def root(id):
self.fail_safe = fail_safe
self.bypass = bypass
self.bypass_caching = bypass_caching
self.caching_limit = caching_limit
self.cache = {}
self._app = app
self.passive = passive
self.static = static
self.hashing = get_optimized_hashing()
self.cache = MemoryCache(self.get_endpoint, caching_limit)
self.parser = Parser(parsers, fail_safe)
self.parser.update_runtime_options(html, js, cssless, script_types)

app and self.init_app(app)

@staticmethod
def get_endpoint_matches(endpoint, patterns):
"""Get the patterns that matches the current endpoint.

Parameters
----------
endpoint: str
to finds the matches for.
patterns: list
regex patterns or strings to match endpoint.
def get_endpoint(self):
"""Get the current response endpoint, with a failsafe.

Returns
-------
list
patterns that match the current endpoint.
str
the current endpoint.
"""
matches, x = tee(
compiled_pattern
for compiled_pattern in (compile_re(p) for p in patterns)
if compiled_pattern.search(endpoint)
)
has_matches = bool(next(x, 0))
with self.app.app_context():
path = getattr(request, "endpoint", "") or ""

return matches, has_matches
if path == "static":
path = getattr(request, "path", "") or ""

return path

@property
def app(self):
Expand All @@ -131,23 +121,6 @@ def app(self):
"""
return self._app or (_app_ctx_stack.top and _app_ctx_stack.top.app)

@property
def endpoint(self):
"""Get the current response endpoint, with a failsafe.

Returns
-------
str
the current endpoint.
"""
with self.app.app_context():
path = getattr(request, "endpoint", "") or ""

if path == "static":
path = getattr(request, "path", "") or ""

return path

def init_app(self, app):
"""Handle initiation of multiple apps NOTE:Factory Method"""
app.after_request(self.main)
Expand All @@ -172,27 +145,34 @@ def get_minified_or_cached(self, content, tag):
str
stored or restored minifed content.
"""
_, bypassed = self.get_endpoint_matches(self.bypass_caching)
get_minified = lambda: self.parser.minify(content, tag)

def _cache_dict():
return self.cache.get(self.endpoint, {})
if bypassed:
return get_minified()

key = self.hashing(content.encode("utf-8")).hexdigest()
limit_reached = len(_cache_dict()) >= self.caching_limit
_, bypassed = self.get_endpoint_matches(self.endpoint, self.bypass_caching)
return self.cache.get_or_set(content, get_minified)

def _cached():
return _cache_dict().get(key)

def _minified():
return self.parser.minify(content, tag)
def get_endpoint_matches(self, patterns):
"""Get the patterns that matches the current endpoint.

if not _cached() and not bypassed:
if limit_reached and _cache_dict():
_cache_dict().popitem()
Parameters
----------
patterns: list
regex patterns or strings to match endpoint.

self.cache.setdefault(self.endpoint, {}).update({key: _minified()})
Returns
-------
(iterable, bool)
patterns that match the current endpoint, and True if any matches found
"""
endpoint = self.get_endpoint()
matches, duplicates = tee(
p for p in map(compile_re, patterns) if p.search(endpoint)
)
has_matches = next(duplicates, 0) != 0

return _cached() or _minified()
return matches, has_matches

def main(self, response):
"""Where a dragon once lived!
Expand All @@ -207,25 +187,20 @@ def main(self, response):
Flask.Response
minified flask response if it fits the requirements.
"""
html = is_html(response)
cssless = is_cssless(response)
js = is_js(response)
_, bypassed = self.get_endpoint_matches(self.endpoint, self.bypass)
_, bypassed = self.get_endpoint_matches(self.bypass)
should_bypass = bypassed or self.passive
should_minify = any(
[
html and self.html,
cssless and self.cssless,
js and self.js,
]
html, cssless, js = does_content_type_match(response)
should_minify = (
(html and self.html) or (cssless and self.cssless) or (js and self.js)
)

if should_minify and not should_bypass:
response.direct_passthrough = False
text = response.get_data(as_text=True)
tag = "html" if html else "script" if js else "style"
if html or (self.static and (cssless or js)):
response.direct_passthrough = False
content = response.get_data(as_text=True)
tag = "html" if html else "script" if js else "style"
minified = self.get_minified_or_cached(content, tag)

if html or (self.static and any([cssless, js])):
response.set_data(self.get_minified_or_cached(text, tag))
response.set_data(minified)

return response
46 changes: 9 additions & 37 deletions flask_minify/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,52 +86,24 @@ def get_tag_contents(html, tag, script_types):
)


def is_html(response):
"""Check if Flask response of HTML content-type.
def does_content_type_match(response):
"""Check if Flask response of content-type match HTML, CSS\LESS or JS.

Parameters
----------
response: Flask response

Returns
-------
True if valid False if not.
(bool, bool, bool)
html, cssless, js if content type match.
"""
content_type = getattr(response, "content_type", "")
content_type = getattr(response, "content_type", "").lower()
html = "text/html" in content_type
cssless = "css" in content_type or "less" in content_type
js = "javascript" in content_type

return "text/html" in content_type.lower()


def is_js(response):
"""Check if Flask response of JS content-type.

Parameters
----------
response: Flask response

Returns
-------
True if valid False if not.
"""
content_type = getattr(response, "content_type", "")

return "javascript" in content_type.lower()


def is_cssless(response):
"""Check if Flask response of Css or Less content-type.

Parameters
----------
response: Flask response

Returns
-------
True if valid False if not.
"""
content_type = getattr(response, "content_type", "")

return "css" in content_type.lower() or "less" in content_type.lower()
return html, cssless, js


def get_optimized_hashing():
Expand Down
Loading