<commit_before># coding=utf-8
from __future__ import absolute_import, division, print_function
from flask import make_response

__author__ = "<NAME> <<EMAIL>>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"

import tornado.web
import flask
import flask.ext.login
import flask.ext.principal
import flask.ext.assets
import webassets.updater
import webassets.utils
import functools
import contextlib
import time
import uuid
import threading
import logging
import netaddr
import os

from octoprint.settings import settings
import octoprint.server
import octoprint.users
import octoprint.plugin

from werkzeug.contrib.cache import BaseCache

try:
	from os import scandir, walk
except ImportError:
	from scandir import scandir, walk

#~~ monkey patching

def enable_additional_translations(default_locale="en", additional_folders=None):
	import os
	from flask import _request_ctx_stack
	from babel import support, Locale
	import flask.ext.babel

	if additional_folders is None:
		additional_folders = []

	logger = logging.getLogger(__name__)

	def fixed_list_translations(self):
		"""Returns a list of all the locales translations exist for.  The
		list returned will be filled with actual locale objects and not just
		strings.
		"""
		def list_translations(dirname):
			if not os.path.isdir(dirname):
				return []
			result = []
			for entry in scandir(dirname):
				locale_dir = os.path.join(entry.path, 'LC_MESSAGES')
				if not os.path.isdir(locale_dir):
					continue
				if filter(lambda x: x.name.endswith('.mo'), scandir(locale_dir)):
					result.append(Locale.parse(entry.name))
			if not result:
				result.append(Locale.parse(self._default_locale))
			return result

		dirs = additional_folders + [os.path.join(self.app.root_path, 'translations')]

		result = [Locale.parse(default_locale)]

		for dir in dirs:
			result += list_translations(dir)
		return result

	def fixed_get_translations():
		"""Returns the correct gettext translations that should be used for
		this request.  This will never fail and return a dummy translation
		object if used outside of the request or if a translation cannot be
		found.
		"""
		ctx = _request_ctx_stack.top
		if ctx is None:
			return None
		translations = getattr(ctx, 'babel_translations', None)
		if translations is None:
			locale = flask.ext.babel.get_locale()
			translations = support.Translations()

			if str(locale) != default_locale:
				# plugin translations
				plugins = octoprint.plugin.plugin_manager().enabled_plugins
				for name, plugin in plugins.items():
					dirs = map(lambda x: os.path.join(x, "_plugins", name), additional_folders) + [os.path.join(plugin.location, 'translations')]
					for dirname in dirs:
						if not os.path.isdir(dirname):
							continue

						try:
							plugin_translations = support.Translations.load(dirname, [locale])
						except:
							logger.exception("Error while trying to load translations for plugin {name}".format(**locals()))
						else:
							if isinstance(plugin_translations, support.Translations):
								translations = translations.merge(plugin_translations)
								logger.debug("Using translation folder {dirname} for locale {locale} of plugin {name}".format(**locals()))
								break
					else:
						logger.debug("No translations for locale {locale} for plugin {name}".format(**locals()))

				# core translations
				dirs = additional_folders + [os.path.join(ctx.app.root_path, 'translations')]
				for dirname in dirs:
					core_translations = support.Translations.load(dirname, [locale])
					if isinstance(core_translations, support.Translations):
						logger.debug("Using translation folder {dirname} for locale {locale} of core translations".format(**locals()))
						break
				else:
					logger.debug("No core translations for locale {locale}")
				translations = translations.merge(core_translations)

			ctx.babel_translations = translations
		return translations

	flask.ext.babel.Babel.list_translations = fixed_list_translations
	flask.ext.babel.get_translations = fixed_get_translations

def fix_webassets_cache():
	from webassets import cache

	error_logger = logging.getLogger(__name__ + ".fix_webassets_cache")

	def fixed_set(self, key, data):
		import os
		import tempfile
		import pickle
		import shutil

		if not os.path.exists(self.directory):
			error_logger.warn("Cache directory {} doesn't exist, not going "
			                  "to attempt to write cache file".format(self.directory))

		md5 = '%s' % cache.make_md5(self.V, key)
		filename = os.path.join(self.directory, md5)
		fd, temp_filename = tempfile.mkstemp(prefix='.' + md5,
		                                     dir=self.directory)
		try:
			with os.fdopen(fd, 'wb') as f:
				pickle.dump(data, f)
				f.flush()
			shutil.move(temp_filename, filename)
		except:
			os.remove(temp_filename)
			raise

	def fixed_get(self, key):
		import os
		import errno
		import warnings
		from webassets.cache import make_md5

		if not os.path.exists(self.directory):
			error_logger.warn("Cache directory {} doesn't exist, not going "
			                  "to attempt to read cache file".format(self.directory))
			return None

		try:
			hash = make_md5(self.V, key)
		except IOError as e:
			if e.errno != errno.ENOENT:
				raise
			return None

		filename = os.path.join(self.directory, '%s' % hash)
		try:
			f = open(filename, 'rb')
		except IOError as e:
			if e.errno != errno.ENOENT:
				error_logger.exception("Got an exception while trying to open webasset file {}".format(filename))
			return None
		try:
			result = f.read()
		finally:
			f.close()

		unpickled = webassets.cache.safe_unpickle(result)
		if unpickled is None:
			warnings.warn('Ignoring corrupted cache file %s' % filename)
		return unpickled

	cache.FilesystemCache.set = fixed_set
	cache.FilesystemCache.get = fixed_get

def fix_webassets_filtertool():
	from webassets.merge import FilterTool, log, MemoryHunk

	error_logger = logging.getLogger(__name__ + ".fix_webassets_filtertool")

	def fixed_wrap_cache(self, key, func):
		"""Return cache value ``key``, or run ``func``.
		"""
		if self.cache:
			if not self.no_cache_read:
				log.debug('Checking cache for key %s', key)
				content = self.cache.get(key)
				if not content in (False, None):
					log.debug('Using cached result for %s', key)
					return MemoryHunk(content)

		try:
			content = func().getvalue()
			if self.cache:
				try:
					log.debug('Storing result in cache with key %s', key,)
					self.cache.set(key, content)
				except:
					error_logger.exception("Got an exception while trying to save file to cache, not caching")
			return MemoryHunk(content)
		except:
			error_logger.exception("Got an exception while trying to apply filter, ignoring file")
			return MemoryHunk(u"")

	FilterTool._wrap_cache = fixed_wrap_cache

#~~ WSGI environment wrapper for reverse proxying

class ReverseProxiedEnvironment(object):

	@staticmethod
	def to_header_candidates(values):
		if values is None:
			return []
		if not isinstance(values, (list, tuple)):
			values = [values]
		to_wsgi_format = lambda header: "HTTP_" + header.upper().replace("-", "_")
		return map(to_wsgi_format, values)

	def __init__(self,
	             header_prefix=None,
	             header_scheme=None,
	             header_host=None,
	             header_server=None,
	             header_port=None,
	             prefix=None,
	             scheme=None,
	             host=None,
	             server=None,
	             port=None):

		# sensible defaults
		if header_prefix is None:
			header_prefix = ["x-script-name"]
		if header_scheme is None:
			header_scheme = ["x-forwarded-proto", "x-scheme"]
		if header_host is None:
			header_host = ["x-forwarded-host"]
		if header_server is None:
			header_server = ["x-forwarded-server"]
		if header_port is None:
			header_port = ["x-forwarded-port"]

		# header candidates
		self._headers_prefix = self.to_header_candidates(header_prefix)
		self._headers_scheme = self.to_header_candidates(header_scheme)
		self._headers_host = self.to_header_candidates(header_host)
		self._headers_server = self.to_header_candidates(header_server)
		self._headers_port = self.to_header_candidates(header_port)

		# fallback prefix & scheme & host from config
		self._fallback_prefix = prefix
		self._fallback_scheme = scheme
		self._fallback_host = host
		self._fallback_server = server
		self._fallback_port = port

	def __call__(self, environ):
		def retrieve_header(header_type):
			candidates = getattr(self, "_headers_" + header_type, [])
			fallback = getattr(self, "_fallback_" + header_type, None)

			for candidate in candidates:
				value = environ.get(candidate, None)
				if value is not None:
					return value
			else:
				return fallback

		def host_to_server_and_port(host, scheme):
			if host is None:
				return None, None

			if ":" in host:
				server, port = host.split(":", 1)
			else:
				server = host
				port = "443" if scheme == "https" else "80"

			return server, port

		# determine prefix
		prefix = retrieve_header("prefix")
		if prefix is not None:
			environ["SCRIPT_NAME"] = prefix
			path_info = environ["PATH_INFO"]
			if path_info.startswith(prefix):
				environ["PATH_INFO"] = path_info[len(prefix):]

		# determine scheme
		scheme = retrieve_header("scheme")
		if scheme is not None and "," in scheme:
			# Scheme might be something like "https,https" if doubly-reverse-proxied
			# without stripping original scheme header first, make sure to only use
			# the first entry in such a case. See #1391.
			scheme, _ = map(lambda x: x.strip(), scheme.split(",", 1))
		if scheme is not None:
			environ["wsgi.url_scheme"] = scheme

		# determine host
		url_scheme = environ["wsgi.url_scheme"]
		host = retrieve_header("host")
		if host is not None:
			# if we have a host, we take server_name and server_port from it
			server, port = host_to_server_and_port(host, url_scheme)
			environ["HTTP_HOST"] = host
			environ["SERVER_NAME"] = server
			environ["SERVER_PORT"] = port

		elif environ.get("HTTP_HOST", None) is not None:
			# if we have a Host header, we use that and make sure our server name and port properties match it
			host = environ["HTTP_HOST"]
			server, port = host_to_server_and_port(host, url_scheme)
			environ["SERVER_NAME"] = server
			environ["SERVER_PORT"] = port

		else:
			# else we take a look at the server and port headers and if we have
			# something there we derive the host from it

			# determine server - should usually not be used
			server = retrieve_header("server")
			if server is not None:
				environ["SERVER_NAME"] = server

			# determine port - should usually not be used
			port = retrieve_header("port")
			if port is not None:
				environ["SERVER_PORT"] = port

			# reconstruct host header
			if url_scheme == "http" and environ["SERVER_PORT"] == "80" or url_scheme == "https" and environ["SERVER_PORT"] == "443":
				# default port for scheme, can be skipped
				environ["HTTP_HOST"] = environ["SERVER_NAME"]
			else:
				environ["HTTP_HOST"] = environ["SERVER_NAME"] + ":" + environ["SERVER_PORT"]

		# call wrapped app with rewritten environment
		return environ

#~~ request and response versions

from werkzeug.wrappers import cached_property

class OctoPrintFlaskRequest(flask.Request):
	environment_wrapper = staticmethod(lambda x: x)

	def __init__(self, environ, *args, **kwargs):
		# apply environment wrapper to provided WSGI environment
		flask.Request.__init__(self, self.environment_wrapper(environ), *args, **kwargs)

	@cached_property
	def cookies(self):
		# strip cookie_suffix from all cookies in the request, return result
		cookies = flask.Request.cookies.__get__(self)

		result = dict()
		desuffixed = dict()
		for key, value in cookies.items():
			if key.endswith(self.cookie_suffix):
				desuffixed[key[:-len(self.cookie_suffix)]] = value
			else:
				result[key] = value

		result.update(desuffixed)
		return result

	@cached_property
	def server_name(self):
		"""Short cut to the request's server name header"""
		return self.environ.get("SERVER_NAME")

	@cached_property
	def server_port(self):
		"""Short cut to the request's server port header"""
		return self.environ.get("SERVER_PORT")

	@cached_property
	def cookie_suffix(self):
		"""
		Request specific suffix for set and read cookies

		We need this because cookies are not port-specific and we don't want to overwrite our
		session and other cookies from one OctoPrint instance on our machine with those of another
		one who happens to listen on the same address albeit a different port.
		"""
		return "_P" + self.server_port


class OctoPrintFlaskResponse(flask.Response):
	def set_cookie(self, key, *args, **kwargs):
		# restrict cookie path to script root
		kwargs["path"] = flask.request.script_root + kwargs.get("path", "/")

		# add request specific cookie suffix to name
		flask.Response.set_cookie(self, key + flask.request.cookie_suffix, *args, **kwargs)

	def delete_cookie(self, key, *args, **kwargs):
		# restrict cookie path to script root
		kwargs["path"] = flask.request.script_root + kwargs.get("path", "/")

		# add request specific cookie suffix to name
		flask.Response.delete_cookie(self, key + flask.request.cookie_suffix, *args, **kwargs)

#~~ passive login helper

def passive_login():
	if octoprint.server.userManager.enabled:
		user = octoprint.server.userManager.login_user(flask.ext.login.current_user)
	else:
		user = flask.ext.login.current_user

	if user is not None and not user.is_anonymous():
		flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id()))
		if hasattr(user, "get_session"):
			flask.session["usersession.id"] = user.get_session()
		flask.g.user = user
		return flask.jsonify(user.asDict())
	elif settings().getBoolean(["accessControl", "autologinLocal"]) \
			and settings().get(["accessControl", "autologinAs"]) is not None \
			and settings().get(["accessControl", "localNetworks"]) is not None:

		autologinAs = settings().get(["accessControl", "autologinAs"])
		localNetworks = netaddr.IPSet([])
		for ip in settings().get(["accessControl", "localNetworks"]):
			localNetworks.add(ip)

		try:
			remoteAddr = get_remote_address(flask.request)
			if netaddr.IPAddress(remoteAddr) in localNetworks:
				user = octoprint.server.userManager.findUser(autologinAs)
				if user is not None:
					user = octoprint.server.userManager.login_user(user)
					flask.session["usersession.id"] = user.get_session()
					flask.g.user = user
					flask.ext.login.login_user(user)
					flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id()))
					return flask.jsonify(user.asDict())
		except:
			logger = logging.getLogger(__name__)
			logger.exception("Could not autologin user %s for networks %r" % (autologinAs, localNetworks))

	return "", 204


#~~ cache decorator for cacheable views

class LessSimpleCache(BaseCache):
	"""
	Slightly improved version of :class:`SimpleCache`.

	Setting ``default_timeout`` or ``timeout`` to ``-1`` will have no timeout be applied at all.
	"""

	def __init__(self, threshold=500, default_timeout=300):
		BaseCache.__init__(self, default_timeout=default_timeout)
		self._cache = {}
		self.clear = self._cache.clear
		self._threshold = threshold

	def _prune(self):
		if self.over_threshold():
			now = time.time()
			for idx, (key, (expires, _)) in enumerate(self._cache.items()):
				if expires is not None and expires <= now or idx % 3 == 0:
					self._cache.pop(key, None)

	def get(self, key):
		import pickle
		now = time.time()
		expires, value = self._cache.get(key, (0, None))
		if expires is None or expires > now:
			return pickle.loads(value)

	def set(self, key, value, timeout=None):
		import pickle
		self._prune()
		self._cache[key] = (self.calculate_timeout(timeout=timeout),
		                    pickle.dumps(value, pickle.HIGHEST_PROTOCOL))

	def add(self, key, value, timeout=None):
		self.set(key, value, timeout=None)
		self._cache.setdefault(key, self._cache[key])

	def delete(self, key):
		self._cache.pop(key, None)

	def calculate_timeout(self, timeout=None):
		if timeout is None:
			timeout = self.default_timeout
		if timeout is -1:
			return None
		return time.time() + timeout

	def over_threshold(self):
		if self._threshold is None:
			return False
		return len(self._cache) > self._threshold

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

	def __setitem__(self, key, value):
		return self.set(key, value)

	def __delitem__(self, key):
		return self.delete(key)

	def __contains__(self, key):
		return key in self._cache

_cache = LessSimpleCache()

def cached(timeout=5 * 60, key=lambda: "view:%s" % flask.request.path, unless=None, refreshif=None, unless_response=None):
	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			logger = logging.getLogger(__name__)

			cache_key = key()

			def f_with_duration(*args, **kwargs):
				start_time = time.time()
				try:
					return f(*args, **kwargs)
				finally:
					elapsed = time.time() - start_time
					logger.debug(
						"Needed {elapsed:.2f}s to render {path} (key: {key})".format(elapsed=elapsed,
						                                                             path=flask.request.path,
						                                                             key=cache_key))

			# bypass the cache if "unless" condition is true
			if callable(unless) and unless():
				logger.debug("Cache for {path} bypassed, calling wrapped function".format(path=flask.request.path))
				return f_with_duration(*args, **kwargs)

			# also bypass the cache if it's disabled completely
			if not settings().getBoolean(["devel", "cache", "enabled"]):
				logger.debug("Cache for {path} disabled, calling wrapped function".format(path=flask.request.path))
				return f_with_duration(*args, **kwargs)

			rv = _cache.get(cache_key)

			# only take the value from the cache if we are not required to refresh it from the wrapped function
			if rv is not None and (not callable(refreshif) or not refreshif(rv)):
				logger.debug("Serving entry for {path} from cache (key: {key})".format(path=flask.request.path, key=cache_key))
				if not "X-From-Cache" in rv.headers:
					rv.headers["X-From-Cache"] = "true"
				return rv

			# get value from wrapped function
			logger.debug("No cache entry or refreshing cache for {path} (key: {key}), calling wrapped function".format(path=flask.request.path, key=cache_key))
			rv = f_with_duration(*args, **kwargs)

			# do not store if the "unless_response" condition is true
			if callable(unless_response) and unless_response(rv):
				logger.debug("Not caching result for {path} (key: {key}), bypassed".format(path=flask.request.path, key=cache_key))
				return rv

			# store it in the cache
			_cache.set(cache_key, rv, timeout=timeout)

			return rv

		return decorated_function

	return decorator

def is_in_cache(key=lambda: "view:%s" % flask.request.path):
	if callable(key):
		key = key()
	return key in _cache

def cache_check_headers():
	return "no-cache" in flask.request.cache_control or "no-cache" in flask.request.pragma

def cache_check_response_headers(response):
	if not isinstance(response, flask.Response):
		return False

	headers = response.headers

	if "Cache-Control" in headers and "no-cache" in headers["Cache-Control"]:
		return True

	if "Pragma" in headers and "no-cache" in headers["Pragma"]:
		return True

	if "Expires" in headers and headers["Expires"] in ("0", "-1"):
		return True

	return False

def cache_check_status_code(response, valid):
	if not isinstance(response, flask.Response):
		return False

	if callable(valid):
		return not valid(response.status_code)
	else:
		return response.status_code not in valid

class PreemptiveCache(object):

	def __init__(self, cachefile):
		self.cachefile = cachefile
		self.environment = None

		self._logger = logging.getLogger(__name__ + "." + self.__class__.__name__)

		self._lock = threading.RLock()

	def record(self, data, unless=None, root=None):
		if callable(unless) and unless():
			return

		entry_data = data
		if callable(entry_data):
			entry_data = entry_data()

		if entry_data is not None:
			if root is None:
				from flask import request
				root = request.path
			self.add_data(root, entry_data)

	def has_record(self, data, root=None):
		if callable(data):
			data = data()

		if data is None:
			return False

		if root is None:
			from flask import request
			root = request.path

		all_data = self.get_data(root)
		for existing in all_data:
			if self._compare_data(data, existing):
				return True

		return False

	def clean_all_data(self, cleanup_function):
		assert callable(cleanup_function)

		with self._lock:
			all_data = self.get_all_data()
			for root, entries in all_data.items():
				old_count = len(entries)
				entries = cleanup_function(root, entries)
				if not entries:
					del all_data[root]
					self._logger.debug("Removed root {} from preemptive cache".format(root))
				elif len(entries) < old_count:
					all_data[root] = entries
					self._logger.debug("Removed {} entries from preemptive cache for root {}".format(old_count - len(entries), root))
			self.set_all_data(all_data)

		return all_data

	def get_all_data(self):
		import yaml

		cache_data = None
		with self._lock:
			try:
				with open(self.cachefile, "r") as f:
					cache_data = yaml.safe_load(f)
			except IOError as e:
				import errno
				if e.errno != errno.ENOENT:
					raise
			except:
				self._logger.exception("Error while reading {}".format(self.cachefile))

		if cache_data is None:
			cache_data = dict()

		return cache_data

	def get_data(self, root):
		cache_data = self.get_all_data()
		return cache_data.get(root, dict())

	def set_all_data(self, data):
		from octoprint.util import atomic_write
		import yaml

		with self._lock:
			try:
				with atomic_write(self.cachefile, "wb") as handle:
					yaml.safe_dump(data, handle,default_flow_style=False, indent="    ", allow_unicode=True)
			except:
				self._logger.exception("Error while writing {}".format(self.cachefile))

	def set_data(self, root, data):
		with self._lock:
			all_data = self.get_all_data()
			all_data[root] = data
			self.set_all_data(all_data)

	def add_data(self, root, data):
		def split_matched_and_unmatched(entry, entries):
			matched = []
			unmatched = []

			for e in entries:
				if self._compare_data(e, entry):
					matched.append(e)
				else:
					unmatched.append(e)

			return matched, unmatched

		with self._lock:
			cache_data = self.get_all_data()

			if not root in cache_data:
				cache_data[root] = []

			existing, other = split_matched_and_unmatched(data, cache_data[root])

			def get_newest(entries):
				result = None
				for entry in entries:
					if "_timestamp" in entry and (result is None or ("_timestamp" in entry and result["_timestamp"] < entry["_timestamp"])):
						result = entry
				return result

			to_persist = get_newest(existing)
			if not to_persist:
				import copy
				to_persist = copy.deepcopy(data)
				to_persist["_timestamp"] = time.time()
				to_persist["_count"] = 1
				self._logger.info("Adding entry for {} and {!r}".format(root, to_persist))
			else:
				to_persist["_timestamp"] = time.time()
				to_persist["_count"] = to_persist.get("_count", 0) + 1
				self._logger.debug("Updating timestamp and counter for {} and {!r}".format(root, data))

			self.set_data(root, [to_persist] + other)

	def _compare_data(self, a, b):
		from octoprint.util import dict_filter

		def strip_ignored(d):
			return dict_filter(d, lambda k, v: not k.startswith("_"))

		return set(strip_ignored(a).items()) == set(strip_ignored(b).items())


def preemptively_cached(cache, data, unless=None):
	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			cache.record(data, unless=unless)
			return f(*args, **kwargs)
		return decorated_function
	return decorator


def etagged(etag):
	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			rv = f(*args, **kwargs)
			if isinstance(rv, flask.Response):
				result = etag
				if callable(result):
					result = result(rv)
				if result:
					rv.set_etag(result)
			return rv
		return decorated_function
	return decorator


def lastmodified(date):
	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			rv = f(*args, **kwargs)
			if not "Last-Modified" in rv.headers:
				result = date
				if callable(result):
					result = result(rv)

				if not isinstance(result, basestring):
					from werkzeug.http import http_date
					result = http_date(result)

				if result:
					rv.headers["Last-Modified"] = result
			return rv
		return decorated_function
	return decorator


def conditional(condition, met):
	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			if callable(condition) and condition():
				# condition has been met, return met-response
				rv = met
				if callable(met):
					rv = met()
				return rv

			# condition hasn't been met, call decorated function
			return f(*args, **kwargs)
		return decorated_function
	return decorator


def with_revalidation_checking(etag_factory=None,
                               lastmodified_factory=None,
                               condition=None,
                               unless=None):
	if etag_factory is None:
		def etag_factory(lm=None):
			return None

	if lastmodified_factory is None:
		def lastmodified_factory():
			return None

	if condition is None:
		def condition(lm=None, etag=None):
			if lm is None:
				lm = lastmodified_factory()

			if etag is None:
				etag = etag_factory(lm=lm)

			return check_lastmodified(lm) and check_etag(etag)

	if unless is None:
		def unless():
			return False

	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			lm = lastmodified_factory()
			etag = etag_factory(lm)

			if condition(lm, etag) and not unless():
				return make_response("Not Modified", 304)

			# generate response
			response = f(*args, **kwargs)

			# set etag header if not already set
			if etag and response.get_etag()[0] is None:
				response.set_etag(etag)

			# set last modified header if not already set
			if lm and response.headers.get("Last-Modified", None) is None:
				if not isinstance(lm, basestring):
					from werkzeug.http import http_date
					lm = http_date(lm)
				response.headers["Last-Modified"] = lm

			response = add_no_max_age_response_headers(response)
			return response
		return decorated_function
	return decorator


def check_etag(etag):
	return flask.request.method in ("GET", "HEAD") and \
	       flask.request.if_none_match is not None and \
	       etag in flask.request.if_none_match


def check_lastmodified(lastmodified):
	if isinstance(lastmodified, float):
		from datetime import datetime
		lastmodified = datetime.fromtimestamp(lastmodified).replace(microsecond=0)<commit_msg>Also accept int values for check_lastmodified<commit_after># coding=utf-8
from __future__ import absolute_import, division, print_function
from flask import make_response

__author__ = "<NAME> <<EMAIL>>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"

import tornado.web
import flask
import flask.ext.login
import flask.ext.principal
import flask.ext.assets
import webassets.updater
import webassets.utils
import functools
import contextlib
import time
import uuid
import threading
import logging
import netaddr
import os

from octoprint.settings import settings
import octoprint.server
import octoprint.users
import octoprint.plugin

from werkzeug.contrib.cache import BaseCache

try:
	from os import scandir, walk
except ImportError:
	from scandir import scandir, walk

#~~ monkey patching

def enable_additional_translations(default_locale="en", additional_folders=None):
	import os
	from flask import _request_ctx_stack
	from babel import support, Locale
	import flask.ext.babel

	if additional_folders is None:
		additional_folders = []

	logger = logging.getLogger(__name__)

	def fixed_list_translations(self):
		"""Returns a list of all the locales translations exist for.  The
		list returned will be filled with actual locale objects and not just
		strings.
		"""
		def list_translations(dirname):
			if not os.path.isdir(dirname):
				return []
			result = []
			for entry in scandir(dirname):
				locale_dir = os.path.join(entry.path, 'LC_MESSAGES')
				if not os.path.isdir(locale_dir):
					continue
				if filter(lambda x: x.name.endswith('.mo'), scandir(locale_dir)):
					result.append(Locale.parse(entry.name))
			if not result:
				result.append(Locale.parse(self._default_locale))
			return result

		dirs = additional_folders + [os.path.join(self.app.root_path, 'translations')]

		result = [Locale.parse(default_locale)]

		for dir in dirs:
			result += list_translations(dir)
		return result

	def fixed_get_translations():
		"""Returns the correct gettext translations that should be used for
		this request.  This will never fail and return a dummy translation
		object if used outside of the request or if a translation cannot be
		found.
		"""
		ctx = _request_ctx_stack.top
		if ctx is None:
			return None
		translations = getattr(ctx, 'babel_translations', None)
		if translations is None:
			locale = flask.ext.babel.get_locale()
			translations = support.Translations()

			if str(locale) != default_locale:
				# plugin translations
				plugins = octoprint.plugin.plugin_manager().enabled_plugins
				for name, plugin in plugins.items():
					dirs = map(lambda x: os.path.join(x, "_plugins", name), additional_folders) + [os.path.join(plugin.location, 'translations')]
					for dirname in dirs:
						if not os.path.isdir(dirname):
							continue

						try:
							plugin_translations = support.Translations.load(dirname, [locale])
						except:
							logger.exception("Error while trying to load translations for plugin {name}".format(**locals()))
						else:
							if isinstance(plugin_translations, support.Translations):
								translations = translations.merge(plugin_translations)
								logger.debug("Using translation folder {dirname} for locale {locale} of plugin {name}".format(**locals()))
								break
					else:
						logger.debug("No translations for locale {locale} for plugin {name}".format(**locals()))

				# core translations
				dirs = additional_folders + [os.path.join(ctx.app.root_path, 'translations')]
				for dirname in dirs:
					core_translations = support.Translations.load(dirname, [locale])
					if isinstance(core_translations, support.Translations):
						logger.debug("Using translation folder {dirname} for locale {locale} of core translations".format(**locals()))
						break
				else:
					logger.debug("No core translations for locale {locale}")
				translations = translations.merge(core_translations)

			ctx.babel_translations = translations
		return translations

	flask.ext.babel.Babel.list_translations = fixed_list_translations
	flask.ext.babel.get_translations = fixed_get_translations

def fix_webassets_cache():
	from webassets import cache

	error_logger = logging.getLogger(__name__ + ".fix_webassets_cache")

	def fixed_set(self, key, data):
		import os
		import tempfile
		import pickle
		import shutil

		if not os.path.exists(self.directory):
			error_logger.warn("Cache directory {} doesn't exist, not going "
			                  "to attempt to write cache file".format(self.directory))

		md5 = '%s' % cache.make_md5(self.V, key)
		filename = os.path.join(self.directory, md5)
		fd, temp_filename = tempfile.mkstemp(prefix='.' + md5,
		                                     dir=self.directory)
		try:
			with os.fdopen(fd, 'wb') as f:
				pickle.dump(data, f)
				f.flush()
			shutil.move(temp_filename, filename)
		except:
			os.remove(temp_filename)
			raise

	def fixed_get(self, key):
		import os
		import errno
		import warnings
		from webassets.cache import make_md5

		if not os.path.exists(self.directory):
			error_logger.warn("Cache directory {} doesn't exist, not going "
			                  "to attempt to read cache file".format(self.directory))
			return None

		try:
			hash = make_md5(self.V, key)
		except IOError as e:
			if e.errno != errno.ENOENT:
				raise
			return None

		filename = os.path.join(self.directory, '%s' % hash)
		try:
			f = open(filename, 'rb')
		except IOError as e:
			if e.errno != errno.ENOENT:
				error_logger.exception("Got an exception while trying to open webasset file {}".format(filename))
			return None
		try:
			result = f.read()
		finally:
			f.close()

		unpickled = webassets.cache.safe_unpickle(result)
		if unpickled is None:
			warnings.warn('Ignoring corrupted cache file %s' % filename)
		return unpickled

	cache.FilesystemCache.set = fixed_set
	cache.FilesystemCache.get = fixed_get

def fix_webassets_filtertool():
	from webassets.merge import FilterTool, log, MemoryHunk

	error_logger = logging.getLogger(__name__ + ".fix_webassets_filtertool")

	def fixed_wrap_cache(self, key, func):
		"""Return cache value ``key``, or run ``func``.
		"""
		if self.cache:
			if not self.no_cache_read:
				log.debug('Checking cache for key %s', key)
				content = self.cache.get(key)
				if not content in (False, None):
					log.debug('Using cached result for %s', key)
					return MemoryHunk(content)

		try:
			content = func().getvalue()
			if self.cache:
				try:
					log.debug('Storing result in cache with key %s', key,)
					self.cache.set(key, content)
				except:
					error_logger.exception("Got an exception while trying to save file to cache, not caching")
			return MemoryHunk(content)
		except:
			error_logger.exception("Got an exception while trying to apply filter, ignoring file")
			return MemoryHunk(u"")

	FilterTool._wrap_cache = fixed_wrap_cache

#~~ WSGI environment wrapper for reverse proxying

class ReverseProxiedEnvironment(object):

	@staticmethod
	def to_header_candidates(values):
		if values is None:
			return []
		if not isinstance(values, (list, tuple)):
			values = [values]
		to_wsgi_format = lambda header: "HTTP_" + header.upper().replace("-", "_")
		return map(to_wsgi_format, values)

	def __init__(self,
	             header_prefix=None,
	             header_scheme=None,
	             header_host=None,
	             header_server=None,
	             header_port=None,
	             prefix=None,
	             scheme=None,
	             host=None,
	             server=None,
	             port=None):

		# sensible defaults
		if header_prefix is None:
			header_prefix = ["x-script-name"]
		if header_scheme is None:
			header_scheme = ["x-forwarded-proto", "x-scheme"]
		if header_host is None:
			header_host = ["x-forwarded-host"]
		if header_server is None:
			header_server = ["x-forwarded-server"]
		if header_port is None:
			header_port = ["x-forwarded-port"]

		# header candidates
		self._headers_prefix = self.to_header_candidates(header_prefix)
		self._headers_scheme = self.to_header_candidates(header_scheme)
		self._headers_host = self.to_header_candidates(header_host)
		self._headers_server = self.to_header_candidates(header_server)
		self._headers_port = self.to_header_candidates(header_port)

		# fallback prefix & scheme & host from config
		self._fallback_prefix = prefix
		self._fallback_scheme = scheme
		self._fallback_host = host
		self._fallback_server = server
		self._fallback_port = port

	def __call__(self, environ):
		def retrieve_header(header_type):
			candidates = getattr(self, "_headers_" + header_type, [])
			fallback = getattr(self, "_fallback_" + header_type, None)

			for candidate in candidates:
				value = environ.get(candidate, None)
				if value is not None:
					return value
			else:
				return fallback

		def host_to_server_and_port(host, scheme):
			if host is None:
				return None, None

			if ":" in host:
				server, port = host.split(":", 1)
			else:
				server = host
				port = "443" if scheme == "https" else "80"

			return server, port

		# determine prefix
		prefix = retrieve_header("prefix")
		if prefix is not None:
			environ["SCRIPT_NAME"] = prefix
			path_info = environ["PATH_INFO"]
			if path_info.startswith(prefix):
				environ["PATH_INFO"] = path_info[len(prefix):]

		# determine scheme
		scheme = retrieve_header("scheme")
		if scheme is not None and "," in scheme:
			# Scheme might be something like "https,https" if doubly-reverse-proxied
			# without stripping original scheme header first, make sure to only use
			# the first entry in such a case. See #1391.
			scheme, _ = map(lambda x: x.strip(), scheme.split(",", 1))
		if scheme is not None:
			environ["wsgi.url_scheme"] = scheme

		# determine host
		url_scheme = environ["wsgi.url_scheme"]
		host = retrieve_header("host")
		if host is not None:
			# if we have a host, we take server_name and server_port from it
			server, port = host_to_server_and_port(host, url_scheme)
			environ["HTTP_HOST"] = host
			environ["SERVER_NAME"] = server
			environ["SERVER_PORT"] = port

		elif environ.get("HTTP_HOST", None) is not None:
			# if we have a Host header, we use that and make sure our server name and port properties match it
			host = environ["HTTP_HOST"]
			server, port = host_to_server_and_port(host, url_scheme)
			environ["SERVER_NAME"] = server
			environ["SERVER_PORT"] = port

		else:
			# else we take a look at the server and port headers and if we have
			# something there we derive the host from it

			# determine server - should usually not be used
			server = retrieve_header("server")
			if server is not None:
				environ["SERVER_NAME"] = server

			# determine port - should usually not be used
			port = retrieve_header("port")
			if port is not None:
				environ["SERVER_PORT"] = port

			# reconstruct host header
			if url_scheme == "http" and environ["SERVER_PORT"] == "80" or url_scheme == "https" and environ["SERVER_PORT"] == "443":
				# default port for scheme, can be skipped
				environ["HTTP_HOST"] = environ["SERVER_NAME"]
			else:
				environ["HTTP_HOST"] = environ["SERVER_NAME"] + ":" + environ["SERVER_PORT"]

		# call wrapped app with rewritten environment
		return environ

#~~ request and response versions

from werkzeug.wrappers import cached_property

class OctoPrintFlaskRequest(flask.Request):
	environment_wrapper = staticmethod(lambda x: x)

	def __init__(self, environ, *args, **kwargs):
		# apply environment wrapper to provided WSGI environment
		flask.Request.__init__(self, self.environment_wrapper(environ), *args, **kwargs)

	@cached_property
	def cookies(self):
		# strip cookie_suffix from all cookies in the request, return result
		cookies = flask.Request.cookies.__get__(self)

		result = dict()
		desuffixed = dict()
		for key, value in cookies.items():
			if key.endswith(self.cookie_suffix):
				desuffixed[key[:-len(self.cookie_suffix)]] = value
			else:
				result[key] = value

		result.update(desuffixed)
		return result

	@cached_property
	def server_name(self):
		"""Short cut to the request's server name header"""
		return self.environ.get("SERVER_NAME")

	@cached_property
	def server_port(self):
		"""Short cut to the request's server port header"""
		return self.environ.get("SERVER_PORT")

	@cached_property
	def cookie_suffix(self):
		"""
		Request specific suffix for set and read cookies

		We need this because cookies are not port-specific and we don't want to overwrite our
		session and other cookies from one OctoPrint instance on our machine with those of another
		one who happens to listen on the same address albeit a different port.
		"""
		return "_P" + self.server_port


class OctoPrintFlaskResponse(flask.Response):
	def set_cookie(self, key, *args, **kwargs):
		# restrict cookie path to script root
		kwargs["path"] = flask.request.script_root + kwargs.get("path", "/")

		# add request specific cookie suffix to name
		flask.Response.set_cookie(self, key + flask.request.cookie_suffix, *args, **kwargs)

	def delete_cookie(self, key, *args, **kwargs):
		# restrict cookie path to script root
		kwargs["path"] = flask.request.script_root + kwargs.get("path", "/")

		# add request specific cookie suffix to name
		flask.Response.delete_cookie(self, key + flask.request.cookie_suffix, *args, **kwargs)

#~~ passive login helper

def passive_login():
	if octoprint.server.userManager.enabled:
		user = octoprint.server.userManager.login_user(flask.ext.login.current_user)
	else:
		user = flask.ext.login.current_user

	if user is not None and not user.is_anonymous():
		flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id()))
		if hasattr(user, "get_session"):
			flask.session["usersession.id"] = user.get_session()
		flask.g.user = user
		return flask.jsonify(user.asDict())
	elif settings().getBoolean(["accessControl", "autologinLocal"]) \
			and settings().get(["accessControl", "autologinAs"]) is not None \
			and settings().get(["accessControl", "localNetworks"]) is not None:

		autologinAs = settings().get(["accessControl", "autologinAs"])
		localNetworks = netaddr.IPSet([])
		for ip in settings().get(["accessControl", "localNetworks"]):
			localNetworks.add(ip)

		try:
			remoteAddr = get_remote_address(flask.request)
			if netaddr.IPAddress(remoteAddr) in localNetworks:
				user = octoprint.server.userManager.findUser(autologinAs)
				if user is not None:
					user = octoprint.server.userManager.login_user(user)
					flask.session["usersession.id"] = user.get_session()
					flask.g.user = user
					flask.ext.login.login_user(user)
					flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id()))
					return flask.jsonify(user.asDict())
		except:
			logger = logging.getLogger(__name__)
			logger.exception("Could not autologin user %s for networks %r" % (autologinAs, localNetworks))

	return "", 204


#~~ cache decorator for cacheable views

class LessSimpleCache(BaseCache):
	"""
	Slightly improved version of :class:`SimpleCache`.

	Setting ``default_timeout`` or ``timeout`` to ``-1`` will have no timeout be applied at all.
	"""

	def __init__(self, threshold=500, default_timeout=300):
		BaseCache.__init__(self, default_timeout=default_timeout)
		self._cache = {}
		self.clear = self._cache.clear
		self._threshold = threshold

	def _prune(self):
		if self.over_threshold():
			now = time.time()
			for idx, (key, (expires, _)) in enumerate(self._cache.items()):
				if expires is not None and expires <= now or idx % 3 == 0:
					self._cache.pop(key, None)

	def get(self, key):
		import pickle
		now = time.time()
		expires, value = self._cache.get(key, (0, None))
		if expires is None or expires > now:
			return pickle.loads(value)

	def set(self, key, value, timeout=None):
		import pickle
		self._prune()
		self._cache[key] = (self.calculate_timeout(timeout=timeout),
		                    pickle.dumps(value, pickle.HIGHEST_PROTOCOL))

	def add(self, key, value, timeout=None):
		self.set(key, value, timeout=None)
		self._cache.setdefault(key, self._cache[key])

	def delete(self, key):
		self._cache.pop(key, None)

	def calculate_timeout(self, timeout=None):
		if timeout is None:
			timeout = self.default_timeout
		if timeout is -1:
			return None
		return time.time() + timeout

	def over_threshold(self):
		if self._threshold is None:
			return False
		return len(self._cache) > self._threshold

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

	def __setitem__(self, key, value):
		return self.set(key, value)

	def __delitem__(self, key):
		return self.delete(key)

	def __contains__(self, key):
		return key in self._cache

_cache = LessSimpleCache()

def cached(timeout=5 * 60, key=lambda: "view:%s" % flask.request.path, unless=None, refreshif=None, unless_response=None):
	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			logger = logging.getLogger(__name__)

			cache_key = key()

			def f_with_duration(*args, **kwargs):
				start_time = time.time()
				try:
					return f(*args, **kwargs)
				finally:
					elapsed = time.time() - start_time
					logger.debug(
						"Needed {elapsed:.2f}s to render {path} (key: {key})".format(elapsed=elapsed,
						                                                             path=flask.request.path,
						                                                             key=cache_key))

			# bypass the cache if "unless" condition is true
			if callable(unless) and unless():
				logger.debug("Cache for {path} bypassed, calling wrapped function".format(path=flask.request.path))
				return f_with_duration(*args, **kwargs)

			# also bypass the cache if it's disabled completely
			if not settings().getBoolean(["devel", "cache", "enabled"]):
				logger.debug("Cache for {path} disabled, calling wrapped function".format(path=flask.request.path))
				return f_with_duration(*args, **kwargs)

			rv = _cache.get(cache_key)

			# only take the value from the cache if we are not required to refresh it from the wrapped function
			if rv is not None and (not callable(refreshif) or not refreshif(rv)):
				logger.debug("Serving entry for {path} from cache (key: {key})".format(path=flask.request.path, key=cache_key))
				if not "X-From-Cache" in rv.headers:
					rv.headers["X-From-Cache"] = "true"
				return rv

			# get value from wrapped function
			logger.debug("No cache entry or refreshing cache for {path} (key: {key}), calling wrapped function".format(path=flask.request.path, key=cache_key))
			rv = f_with_duration(*args, **kwargs)

			# do not store if the "unless_response" condition is true
			if callable(unless_response) and unless_response(rv):
				logger.debug("Not caching result for {path} (key: {key}), bypassed".format(path=flask.request.path, key=cache_key))
				return rv

			# store it in the cache
			_cache.set(cache_key, rv, timeout=timeout)

			return rv

		return decorated_function

	return decorator

def is_in_cache(key=lambda: "view:%s" % flask.request.path):
	if callable(key):
		key = key()
	return key in _cache

def cache_check_headers():
	return "no-cache" in flask.request.cache_control or "no-cache" in flask.request.pragma

def cache_check_response_headers(response):
	if not isinstance(response, flask.Response):
		return False

	headers = response.headers

	if "Cache-Control" in headers and "no-cache" in headers["Cache-Control"]:
		return True

	if "Pragma" in headers and "no-cache" in headers["Pragma"]:
		return True

	if "Expires" in headers and headers["Expires"] in ("0", "-1"):
		return True

	return False

def cache_check_status_code(response, valid):
	if not isinstance(response, flask.Response):
		return False

	if callable(valid):
		return not valid(response.status_code)
	else:
		return response.status_code not in valid

class PreemptiveCache(object):

	def __init__(self, cachefile):
		self.cachefile = cachefile
		self.environment = None

		self._logger = logging.getLogger(__name__ + "." + self.__class__.__name__)

		self._lock = threading.RLock()

	def record(self, data, unless=None, root=None):
		if callable(unless) and unless():
			return

		entry_data = data
		if callable(entry_data):
			entry_data = entry_data()

		if entry_data is not None:
			if root is None:
				from flask import request
				root = request.path
			self.add_data(root, entry_data)

	def has_record(self, data, root=None):
		if callable(data):
			data = data()

		if data is None:
			return False

		if root is None:
			from flask import request
			root = request.path

		all_data = self.get_data(root)
		for existing in all_data:
			if self._compare_data(data, existing):
				return True

		return False

	def clean_all_data(self, cleanup_function):
		assert callable(cleanup_function)

		with self._lock:
			all_data = self.get_all_data()
			for root, entries in all_data.items():
				old_count = len(entries)
				entries = cleanup_function(root, entries)
				if not entries:
					del all_data[root]
					self._logger.debug("Removed root {} from preemptive cache".format(root))
				elif len(entries) < old_count:
					all_data[root] = entries
					self._logger.debug("Removed {} entries from preemptive cache for root {}".format(old_count - len(entries), root))
			self.set_all_data(all_data)

		return all_data

	def get_all_data(self):
		import yaml

		cache_data = None
		with self._lock:
			try:
				with open(self.cachefile, "r") as f:
					cache_data = yaml.safe_load(f)
			except IOError as e:
				import errno
				if e.errno != errno.ENOENT:
					raise
			except:
				self._logger.exception("Error while reading {}".format(self.cachefile))

		if cache_data is None:
			cache_data = dict()

		return cache_data

	def get_data(self, root):
		cache_data = self.get_all_data()
		return cache_data.get(root, dict())

	def set_all_data(self, data):
		from octoprint.util import atomic_write
		import yaml

		with self._lock:
			try:
				with atomic_write(self.cachefile, "wb") as handle:
					yaml.safe_dump(data, handle,default_flow_style=False, indent="    ", allow_unicode=True)
			except:
				self._logger.exception("Error while writing {}".format(self.cachefile))

	def set_data(self, root, data):
		with self._lock:
			all_data = self.get_all_data()
			all_data[root] = data
			self.set_all_data(all_data)

	def add_data(self, root, data):
		def split_matched_and_unmatched(entry, entries):
			matched = []
			unmatched = []

			for e in entries:
				if self._compare_data(e, entry):
					matched.append(e)
				else:
					unmatched.append(e)

			return matched, unmatched

		with self._lock:
			cache_data = self.get_all_data()

			if not root in cache_data:
				cache_data[root] = []

			existing, other = split_matched_and_unmatched(data, cache_data[root])

			def get_newest(entries):
				result = None
				for entry in entries:
					if "_timestamp" in entry and (result is None or ("_timestamp" in entry and result["_timestamp"] < entry["_timestamp"])):
						result = entry
				return result

			to_persist = get_newest(existing)
			if not to_persist:
				import copy
				to_persist = copy.deepcopy(data)
				to_persist["_timestamp"] = time.time()
				to_persist["_count"] = 1
				self._logger.info("Adding entry for {} and {!r}".format(root, to_persist))
			else:
				to_persist["_timestamp"] = time.time()
				to_persist["_count"] = to_persist.get("_count", 0) + 1
				self._logger.debug("Updating timestamp and counter for {} and {!r}".format(root, data))

			self.set_data(root, [to_persist] + other)

	def _compare_data(self, a, b):
		from octoprint.util import dict_filter

		def strip_ignored(d):
			return dict_filter(d, lambda k, v: not k.startswith("_"))

		return set(strip_ignored(a).items()) == set(strip_ignored(b).items())


def preemptively_cached(cache, data, unless=None):
	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			cache.record(data, unless=unless)
			return f(*args, **kwargs)
		return decorated_function
	return decorator


def etagged(etag):
	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			rv = f(*args, **kwargs)
			if isinstance(rv, flask.Response):
				result = etag
				if callable(result):
					result = result(rv)
				if result:
					rv.set_etag(result)
			return rv
		return decorated_function
	return decorator


def lastmodified(date):
	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			rv = f(*args, **kwargs)
			if not "Last-Modified" in rv.headers:
				result = date
				if callable(result):
					result = result(rv)

				if not isinstance(result, basestring):
					from werkzeug.http import http_date
					result = http_date(result)

				if result:
					rv.headers["Last-Modified"] = result
			return rv
		return decorated_function
	return decorator


def conditional(condition, met):
	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			if callable(condition) and condition():
				# condition has been met, return met-response
				rv = met
				if callable(met):
					rv = met()
				return rv

			# condition hasn't been met, call decorated function
			return f(*args, **kwargs)
		return decorated_function
	return decorator


def with_revalidation_checking(etag_factory=None,
                               lastmodified_factory=None,
                               condition=None,
                               unless=None):
	if etag_factory is None:
		def etag_factory(lm=None):
			return None

	if lastmodified_factory is None:
		def lastmodified_factory():
			return None

	if condition is None:
		def condition(lm=None, etag=None):
			if lm is None:
				lm = lastmodified_factory()

			if etag is None:
				etag = etag_factory(lm=lm)

			return check_lastmodified(lm) and check_etag(etag)

	if unless is None:
		def unless():
			return False

	def decorator(f):
		@functools.wraps(f)
		def decorated_function(*args, **kwargs):
			lm = lastmodified_factory()
			etag = etag_factory(lm)

			if condition(lm, etag) and not unless():
				return make_response("Not Modified", 304)

			# generate response
			response = f(*args, **kwargs)

			# set etag header if not already set
			if etag and response.get_etag()[0] is None:
				response.set_etag(etag)

			# set last modified header if not already set
			if lm and response.headers.get("Last-Modified", None) is None:
				if not isinstance(lm, basestring):
					from werkzeug.http import http_date
					lm = http_date(lm)
				response.headers["Last-Modified"] = lm

			response = add_no_max_age_response_headers(response)
			return response
		return decorated_function
	return decorator


def check_etag(etag):
	return flask.request.method in ("GET", "HEAD") and \
	       flask.request.if_none_match is not None and \
	       etag in flask.request.if_none_match


def check_lastmodified(lastmodified):
	from datetime import datetime
	if isinstance(lastmodified, (float, int)):
		lastmodified = datetime.fromtimestamp(lastmodified).replace(microsecond=0)

	if not isinstance(lastmodified, datetime):
		raise ValueError("lastmodified must be a datetime or float or int instance but, got {} instead".format(lastmodified.__class__))
{'fraction_non_alphanumeric': 0.10789236513454359, 'fraction_numerical': 0.002059997425003219, 'mean_word_length': 3.8230284751175376, 'pattern_counts': {'":': 8, '<': 15, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2868618', 'n_tokens_mistral': 19725, 'n_tokens_neox': 16872, 'n_words': 4765}