Skip to content

Commit

Permalink
Merge pull request #69 from mrf345/testing
Browse files Browse the repository at this point in the history
Add caching module and refactor code
  • Loading branch information
mrf345 authored Dec 15, 2021
2 parents eb2b713 + 5d5730b commit 5f8bab3
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 151 deletions.
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

0 comments on commit 5f8bab3

Please sign in to comment.