appier 1.32.0__tar.gz → 1.33.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. {appier-1.32.0/src/appier.egg-info → appier-1.33.0}/PKG-INFO +1 -1
  2. {appier-1.32.0 → appier-1.33.0}/setup.py +1 -1
  3. {appier-1.32.0 → appier-1.33.0}/src/appier/__init__.py +1 -1
  4. {appier-1.32.0 → appier-1.33.0}/src/appier/base.py +48 -8
  5. {appier-1.32.0 → appier-1.33.0}/src/appier/legacy.py +33 -0
  6. {appier-1.32.0 → appier-1.33.0}/src/appier/model.py +8 -6
  7. appier-1.33.0/src/appier/scheduler.py +287 -0
  8. {appier-1.32.0 → appier-1.33.0}/src/appier/test/base.py +19 -1
  9. appier-1.33.0/src/appier/test/scheduler.py +173 -0
  10. {appier-1.32.0 → appier-1.33.0}/src/appier/test/typesf.py +1 -1
  11. appier-1.33.0/src/appier/test/validation.py +119 -0
  12. {appier-1.32.0 → appier-1.33.0}/src/appier/util.py +3 -3
  13. {appier-1.32.0 → appier-1.33.0}/src/appier/validation.py +8 -8
  14. {appier-1.32.0 → appier-1.33.0/src/appier.egg-info}/PKG-INFO +1 -1
  15. {appier-1.32.0 → appier-1.33.0}/src/appier.egg-info/SOURCES.txt +3 -1
  16. appier-1.32.0/src/appier/scheduler.py +0 -95
  17. {appier-1.32.0 → appier-1.33.0}/MANIFEST.in +0 -0
  18. {appier-1.32.0 → appier-1.33.0}/README.rst +0 -0
  19. {appier-1.32.0 → appier-1.33.0}/setup.cfg +0 -0
  20. {appier-1.32.0 → appier-1.33.0}/src/appier/amqp.py +0 -0
  21. {appier-1.32.0 → appier-1.33.0}/src/appier/api.py +0 -0
  22. {appier-1.32.0 → appier-1.33.0}/src/appier/asgi.py +0 -0
  23. {appier-1.32.0 → appier-1.33.0}/src/appier/async_neo.py +0 -0
  24. {appier-1.32.0 → appier-1.33.0}/src/appier/async_old.py +0 -0
  25. {appier-1.32.0 → appier-1.33.0}/src/appier/asynchronous.py +0 -0
  26. {appier-1.32.0 → appier-1.33.0}/src/appier/bus.py +0 -0
  27. {appier-1.32.0 → appier-1.33.0}/src/appier/cache.py +0 -0
  28. {appier-1.32.0 → appier-1.33.0}/src/appier/common.py +0 -0
  29. {appier-1.32.0 → appier-1.33.0}/src/appier/component.py +0 -0
  30. {appier-1.32.0 → appier-1.33.0}/src/appier/compress.py +0 -0
  31. {appier-1.32.0 → appier-1.33.0}/src/appier/config.py +0 -0
  32. {appier-1.32.0 → appier-1.33.0}/src/appier/controller.py +0 -0
  33. {appier-1.32.0 → appier-1.33.0}/src/appier/crypt.py +0 -0
  34. {appier-1.32.0 → appier-1.33.0}/src/appier/data.py +0 -0
  35. {appier-1.32.0 → appier-1.33.0}/src/appier/defines.py +0 -0
  36. {appier-1.32.0 → appier-1.33.0}/src/appier/exceptions.py +0 -0
  37. {appier-1.32.0 → appier-1.33.0}/src/appier/execution.py +0 -0
  38. {appier-1.32.0 → appier-1.33.0}/src/appier/export.py +0 -0
  39. {appier-1.32.0 → appier-1.33.0}/src/appier/extra.py +0 -0
  40. {appier-1.32.0 → appier-1.33.0}/src/appier/extra_neo.py +0 -0
  41. {appier-1.32.0 → appier-1.33.0}/src/appier/extra_old.py +0 -0
  42. {appier-1.32.0 → appier-1.33.0}/src/appier/geo.py +0 -0
  43. {appier-1.32.0 → appier-1.33.0}/src/appier/git.py +0 -0
  44. {appier-1.32.0 → appier-1.33.0}/src/appier/graph.py +0 -0
  45. {appier-1.32.0 → appier-1.33.0}/src/appier/http.py +0 -0
  46. {appier-1.32.0 → appier-1.33.0}/src/appier/log.py +0 -0
  47. {appier-1.32.0 → appier-1.33.0}/src/appier/meta.py +0 -0
  48. {appier-1.32.0 → appier-1.33.0}/src/appier/mock.py +0 -0
  49. {appier-1.32.0 → appier-1.33.0}/src/appier/model_a.py +0 -0
  50. {appier-1.32.0 → appier-1.33.0}/src/appier/mongo.py +0 -0
  51. {appier-1.32.0 → appier-1.33.0}/src/appier/observer.py +0 -0
  52. {appier-1.32.0 → appier-1.33.0}/src/appier/part.py +0 -0
  53. {appier-1.32.0 → appier-1.33.0}/src/appier/preferences.py +0 -0
  54. {appier-1.32.0 → appier-1.33.0}/src/appier/queuing.py +0 -0
  55. {appier-1.32.0 → appier-1.33.0}/src/appier/redisdb.py +0 -0
  56. {appier-1.32.0 → appier-1.33.0}/src/appier/request.py +0 -0
  57. {appier-1.32.0 → appier-1.33.0}/src/appier/res/static/css/base.css +0 -0
  58. {appier-1.32.0 → appier-1.33.0}/src/appier/res/static/images/favicon.ico +0 -0
  59. {appier-1.32.0 → appier-1.33.0}/src/appier/res/static/js/base.js +0 -0
  60. {appier-1.32.0 → appier-1.33.0}/src/appier/res/templates/error.html.tpl +0 -0
  61. {appier-1.32.0 → appier-1.33.0}/src/appier/res/templates/holder.html.tpl +0 -0
  62. {appier-1.32.0 → appier-1.33.0}/src/appier/res/templates/layout.html.tpl +0 -0
  63. {appier-1.32.0 → appier-1.33.0}/src/appier/serialize.py +0 -0
  64. {appier-1.32.0 → appier-1.33.0}/src/appier/session.py +0 -0
  65. {appier-1.32.0 → appier-1.33.0}/src/appier/settings.py +0 -0
  66. {appier-1.32.0 → appier-1.33.0}/src/appier/smtp.py +0 -0
  67. {appier-1.32.0 → appier-1.33.0}/src/appier/storage.py +0 -0
  68. {appier-1.32.0 → appier-1.33.0}/src/appier/structures.py +0 -0
  69. {appier-1.32.0 → appier-1.33.0}/src/appier/test/__init__.py +0 -0
  70. {appier-1.32.0 → appier-1.33.0}/src/appier/test/cache.py +0 -0
  71. {appier-1.32.0 → appier-1.33.0}/src/appier/test/config.py +0 -0
  72. {appier-1.32.0 → appier-1.33.0}/src/appier/test/crypt.py +0 -0
  73. {appier-1.32.0 → appier-1.33.0}/src/appier/test/data.py +0 -0
  74. {appier-1.32.0 → appier-1.33.0}/src/appier/test/exceptions.py +0 -0
  75. {appier-1.32.0 → appier-1.33.0}/src/appier/test/export.py +0 -0
  76. {appier-1.32.0 → appier-1.33.0}/src/appier/test/graph.py +0 -0
  77. {appier-1.32.0 → appier-1.33.0}/src/appier/test/http.py +0 -0
  78. {appier-1.32.0 → appier-1.33.0}/src/appier/test/legacy.py +0 -0
  79. {appier-1.32.0 → appier-1.33.0}/src/appier/test/log.py +0 -0
  80. {appier-1.32.0 → appier-1.33.0}/src/appier/test/mock.py +0 -0
  81. {appier-1.32.0 → appier-1.33.0}/src/appier/test/model.py +0 -0
  82. {appier-1.32.0 → appier-1.33.0}/src/appier/test/part.py +0 -0
  83. {appier-1.32.0 → appier-1.33.0}/src/appier/test/preferences.py +0 -0
  84. {appier-1.32.0 → appier-1.33.0}/src/appier/test/queuing.py +0 -0
  85. {appier-1.32.0 → appier-1.33.0}/src/appier/test/request.py +0 -0
  86. {appier-1.32.0 → appier-1.33.0}/src/appier/test/serialize.py +0 -0
  87. {appier-1.32.0 → appier-1.33.0}/src/appier/test/session.py +0 -0
  88. {appier-1.32.0 → appier-1.33.0}/src/appier/test/smtp.py +0 -0
  89. {appier-1.32.0 → appier-1.33.0}/src/appier/test/structures.py +0 -0
  90. {appier-1.32.0 → appier-1.33.0}/src/appier/test/util.py +0 -0
  91. {appier-1.32.0 → appier-1.33.0}/src/appier/typesf.py +0 -0
  92. {appier-1.32.0 → appier-1.33.0}/src/appier.egg-info/dependency_links.txt +0 -0
  93. {appier-1.32.0 → appier-1.33.0}/src/appier.egg-info/not-zip-safe +0 -0
  94. {appier-1.32.0 → appier-1.33.0}/src/appier.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.1
2
2
  Name: appier
3
- Version: 1.32.0
3
+ Version: 1.33.0
4
4
  Summary: Appier Framework
5
5
  Home-page: http://appier.hive.pt
6
6
  Author: Hive Solutions Lda.
@@ -44,7 +44,7 @@ def read_file(path):
44
44
 
45
45
  setuptools.setup(
46
46
  name="appier",
47
- version="1.32.0",
47
+ version="1.33.0",
48
48
  author="Hive Solutions Lda.",
49
49
  author_email="development@hive.pt",
50
50
  description="Appier Framework",
@@ -268,7 +268,7 @@ from .preferences import (
268
268
  from .queuing import Queue, MemoryQueue, MultiprocessQueue, AMQPQueue
269
269
  from .redisdb import Redis
270
270
  from .request import CODE_STRINGS, Request, MockRequest
271
- from .scheduler import Scheduler
271
+ from .scheduler import Scheduler, CronScheduler, SchedulerTask, SchedulerDate, Cron
272
272
  from .serialize import serialize_csv, serialize_ics, build_encoder
273
273
  from .session import (
274
274
  Session,
@@ -68,6 +68,7 @@ from . import compress
68
68
  from . import settings
69
69
  from . import observer
70
70
  from . import execution
71
+ from . import scheduler
71
72
  from . import controller
72
73
  from . import structures
73
74
  from . import exceptions
@@ -93,7 +94,7 @@ NAME = "appier"
93
94
  """ The name to be used to describe the framework while working
94
95
  on its own environment, this is just a descriptive value """
95
96
 
96
- VERSION = "1.32.0"
97
+ VERSION = "1.33.0"
97
98
  """ The version of the framework that is currently installed
98
99
  this value may be used for debugging/diagnostic purposes """
99
100
 
@@ -194,17 +195,17 @@ OCTET_TYPE = "application/octet-stream"
194
195
  """ The mime/content type to be used for octet stream based message payloads
195
196
  so that the legacy byte oriented value is readable """
196
197
 
197
- REPLACE_REGEX = re.compile("(?<!\(\?P)\<((\w+)(\([\"'].*?[\"']\))?:)?(\w+)\>")
198
+ REPLACE_REGEX = re.compile(r"(?<!\(\?P)\<((\w+)(\([\"'].*?[\"']\))?:)?(\w+)\>")
198
199
  """ The regular expression to be used in the replacement
199
200
  of the capture groups for the urls, this regex will capture
200
201
  any named group not changed until this stage (eg: int,
201
202
  string, regex, etc.) """
202
203
 
203
- INT_REGEX = re.compile("\<int:(\w+)\>")
204
+ INT_REGEX = re.compile(r"\<int:(\w+)\>")
204
205
  """ The regular expression to be used in the replacement
205
206
  of the integer type based groups for the urls """
206
207
 
207
- REGEX_REGEX = re.compile("\<regex\([\"'](.*?)[\"']\):(\w+)\>")
208
+ REGEX_REGEX = re.compile(r"\<regex\([\"'](.*?)[\"']\):(\w+)\>")
208
209
  """ Regular expression that is going to be used for the
209
210
  replacement of regular expression types with the proper
210
211
  group in the final URL based route regex """
@@ -217,7 +218,9 @@ SLUGIER_REGEX_2 = re.compile(r"[-]+", re.UNICODE) # @UndefinedVariable
217
218
  """ The second regular expression that is going to be used
218
219
  by the slugier sub system to replace some of its values """
219
220
 
220
- CSS_ABS_REGEX = re.compile(b"url\((?!(http:\/\/|https:\/\/|\/\/|\/))([^\)]+)\)")
221
+ CSS_ABS_REGEX = re.compile(
222
+ legacy.bytes(r"url\((?!(http:\/\/|https:\/\/|\/\/|\/))([^\)]+)\)")
223
+ )
221
224
  """ The regular expression that is going to be used to capture
222
225
  the relative CSS URL values, so that they may be converted into
223
226
  absolute ones for proper inlining, note that the regex is defined
@@ -425,6 +428,7 @@ class App(
425
428
  self._user_routes = None
426
429
  self._core_routes = None
427
430
  self._own = self
431
+ self._cron = None
428
432
  self._peers = {}
429
433
  self.__routes = []
430
434
  self.load(level=level, handlers=handlers)
@@ -730,6 +734,7 @@ class App(
730
734
  self._start_controllers()
731
735
  self._start_models()
732
736
  self._start_supervisor()
737
+ self._start_cron()
733
738
  if refresh:
734
739
  self.refresh()
735
740
  self.status = RUNNING
@@ -741,6 +746,7 @@ class App(
741
746
  self._print_bye()
742
747
  if refresh:
743
748
  self.refresh()
749
+ self._stop_cron()
744
750
  self._stop_supervisor()
745
751
  self._stop_models()
746
752
  self._stop_controllers()
@@ -2321,6 +2327,30 @@ class App(
2321
2327
  thread.daemon = True
2322
2328
  thread.start()
2323
2329
 
2330
+ def cron(self, job, cron):
2331
+ """
2332
+ Schedule the provided method for regular execution using
2333
+ the provided Cron like string.
2334
+
2335
+ The method is going to be executed at the provided time
2336
+ in a separate thread.
2337
+
2338
+ :type job: function
2339
+ :param job: The function/method that is going to be executed
2340
+ at the specified time using the cron like string.
2341
+ :type cron: String/SchedulerDate
2342
+ :param cron: The cron like string that is going to be used to
2343
+ define the execution time of the provided method.
2344
+ :rtype: SchedulerTask
2345
+ :return: The task that has been scheduled for execution at the
2346
+ provided time.
2347
+ """
2348
+
2349
+ if self._cron == None:
2350
+ self._cron = scheduler.CronScheduler(self)
2351
+ self._cron.start()
2352
+ return self._cron.schedule(job, cron)
2353
+
2324
2354
  def chunks(self, data, size=32768):
2325
2355
  for index in range(0, len(data), size):
2326
2356
  yield data[index : index + size]
@@ -2618,7 +2648,7 @@ class App(
2618
2648
  self.template_args(kwargs)
2619
2649
 
2620
2650
  # verifies if the target locale for the template has been defined
2621
- # and if thtat's the case updates the keyword based arguments for
2651
+ # and if that's the case updates the keyword based arguments for
2622
2652
  # the current template render to include that value
2623
2653
  if locale:
2624
2654
  kwargs["_locale"] = locale
@@ -3763,8 +3793,8 @@ class App(
3763
3793
  timeout = int(max_age)
3764
3794
 
3765
3795
  # in case the type of the resource is css an extra replace operation
3766
- # on the urls must be performed so that the base URL is added to all
3767
- # the resources, this is required so that relative urls are fixed
3796
+ # on the URLs must be performed so that the base URL is added to all
3797
+ # the resources, this is required so that relative URLs are fixed
3768
3798
  if type == "css":
3769
3799
  base, _name = url.rsplit("/", 1)
3770
3800
  base = legacy.bytes(base)
@@ -5356,6 +5386,16 @@ class App(
5356
5386
  def _stop_supervisor(self):
5357
5387
  pass
5358
5388
 
5389
+ def _start_cron(self):
5390
+ pass
5391
+
5392
+ def _stop_cron(self):
5393
+ if not self._cron:
5394
+ return
5395
+ self._cron.stop()
5396
+ self._cron.join()
5397
+ self._cron = None
5398
+
5359
5399
  def _add_route(self, *args, **kwargs):
5360
5400
  self.__routes.append(args)
5361
5401
  self.clear_routes()
@@ -30,6 +30,8 @@ __license__ = "Apache License, Version 2.0"
30
30
 
31
31
  import os
32
32
  import sys
33
+ import calendar
34
+ import datetime
33
35
  import inspect
34
36
  import functools
35
37
  import itertools
@@ -152,6 +154,10 @@ PYTHON_3 = sys.version_info[0] >= 3
152
154
  interpreter is at least Python 3 compliant, this is used
153
155
  to take some of the conversion decision for runtime """
154
156
 
157
+ PYTHON_33 = sys.version_info[0] >= 3 and sys.version_info[1] >= 3
158
+ """ Global variable that defines if the current Python
159
+ interpreter is at least Python 3.3 compliant """
160
+
155
161
  PYTHON_35 = sys.version_info[0] >= 3 and sys.version_info[1] >= 5
156
162
  """ Global variable that defines if the current Python
157
163
  interpreter is at least Python 3.5 compliant """
@@ -538,6 +544,33 @@ def build_opener(*args, **kwargs):
538
544
  return urllib2.build_opener(*args, **kwargs) # @UndefinedVariable
539
545
 
540
546
 
547
+ def to_timestamp(date_time):
548
+ if PYTHON_33:
549
+ return date_time.replace(tzinfo=datetime.timezone.utc).timestamp()
550
+ else:
551
+ return calendar.timegm(date_time.utctimetuple())
552
+
553
+
554
+ def to_datetime(timestamp):
555
+ if PYTHON_33:
556
+ return datetime.datetime.fromtimestamp(
557
+ timestamp, datetime.timezone.utc
558
+ ).replace(tzinfo=None)
559
+ else:
560
+ return datetime.datetime.utcfromtimestamp(timestamp)
561
+
562
+
563
+ def utcfromtimestamp(timestamp):
564
+ return to_datetime(timestamp)
565
+
566
+
567
+ def utc_now():
568
+ if PYTHON_33:
569
+ return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
570
+ else:
571
+ return datetime.datetime.utcnow()
572
+
573
+
541
574
  def urlparse(*args, **kwargs):
542
575
  return _urlparse.urlparse(*args, **kwargs)
543
576
 
@@ -56,12 +56,14 @@ RE = lambda v: [i for i in v if not i == ""]
56
56
  empty element from the provided list values """
57
57
 
58
58
  BUILDERS = {
59
- legacy.UNICODE: lambda v: v.decode("utf-8")
60
- if isinstance(v, legacy.BYTES)
61
- else legacy.UNICODE(v),
62
- list: lambda v: RE(v)
63
- if isinstance(v, list)
64
- else (json.loads(v) if isinstance(v, legacy.UNICODE) else RE([v])),
59
+ legacy.UNICODE: lambda v: (
60
+ v.decode("utf-8") if isinstance(v, legacy.BYTES) else legacy.UNICODE(v)
61
+ ),
62
+ list: lambda v: (
63
+ RE(v)
64
+ if isinstance(v, list)
65
+ else (json.loads(v) if isinstance(v, legacy.UNICODE) else RE([v]))
66
+ ),
65
67
  dict: lambda v: json.loads(v) if isinstance(v, legacy.UNICODE) else dict(v),
66
68
  bool: lambda v: v if isinstance(v, bool) else not v in ("", "0", "false", "False"),
67
69
  }
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # Hive Appier Framework
5
+ # Copyright (c) 2008-2024 Hive Solutions Lda.
6
+ #
7
+ # This file is part of Hive Appier Framework.
8
+ #
9
+ # Hive Appier Framework is free software: you can redistribute it and/or modify
10
+ # it under the terms of the Apache License as published by the Apache
11
+ # Foundation, either version 2.0 of the License, or (at your option) any
12
+ # later version.
13
+ #
14
+ # Hive Appier Framework is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # Apache License for more details.
18
+ #
19
+ # You should have received a copy of the Apache License along with
20
+ # Hive Appier Framework. If not, see <http://www.apache.org/licenses/>.
21
+
22
+ __author__ = "João Magalhães <joamag@hive.pt>"
23
+ """ The author(s) of the module """
24
+
25
+ __copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda."
26
+ """ The copyright for the module """
27
+
28
+ __license__ = "Apache License, Version 2.0"
29
+ """ The license for the module """
30
+
31
+ import time
32
+ import heapq
33
+ import calendar
34
+ import datetime
35
+ import logging
36
+ import threading
37
+ import traceback
38
+
39
+ from . import config
40
+ from . import legacy
41
+
42
+ LOOP_TIMEOUT = 60.0
43
+ """ The time value to be used to sleep the main sequence
44
+ loop between ticks, this value should not be too small
45
+ to spend many resources or to high to create a long set
46
+ of time between external interactions """
47
+
48
+
49
+ class Scheduler(threading.Thread):
50
+ """
51
+ Scheduler class that handles timeout based async tasks
52
+ within the context of an Appier application.
53
+
54
+ The architecture of the logic for the class should be
55
+ modular in the sense that new task may be added to
56
+ it through a queue or other external system. For that
57
+ a proper preemption mechanism should exist allowing
58
+ the scheduler to be stopped and started again.
59
+ """
60
+
61
+ def __init__(self, owner, timeout=LOOP_TIMEOUT, daemon=True):
62
+ threading.Thread.__init__(self, name="Scheduler")
63
+ self.owner = owner
64
+ self.timeout = config.conf("SCHEDULER_TIMEOUT", timeout, cast=float)
65
+ self.daemon = config.conf("SCHEDULER_DAEMON", daemon, cast=bool)
66
+ self._condition = threading.Condition()
67
+
68
+ def run(self):
69
+ self.running = True
70
+ self.load()
71
+ while self.running:
72
+ try:
73
+ self.tick()
74
+ except Exception as exception:
75
+ self.logger.critical("Unhandled scheduler exception raised")
76
+ self.logger.error(exception)
77
+ lines = traceback.format_exc().splitlines()
78
+ for line in lines:
79
+ self.logger.warning(line)
80
+ self._condition.acquire()
81
+ self._condition.wait(self.timeout)
82
+ self._condition.release()
83
+
84
+ def stop(self, awake=True):
85
+ self.running = False
86
+ if awake:
87
+ self.awake()
88
+
89
+ def tick(self):
90
+ pass
91
+
92
+ def load(self):
93
+ pass
94
+
95
+ def awake(self):
96
+ self._condition.acquire()
97
+ self._condition.notify()
98
+ self._condition.release()
99
+
100
+ @property
101
+ def logger(self):
102
+ if self.owner:
103
+ return self.owner.logger
104
+ else:
105
+ return logging.getLogger()
106
+
107
+
108
+ class CronScheduler(Scheduler):
109
+ """
110
+ Specialized version of the scheduler that runs tasks
111
+ based on a cron like configuration.
112
+
113
+ The tasks are defined in a cron like format and are
114
+ executed based on the current time.
115
+ """
116
+
117
+ def __init__(self, owner, timeout=LOOP_TIMEOUT, daemon=True):
118
+ Scheduler.__init__(self, owner, timeout=timeout, daemon=daemon)
119
+ self._tasks = []
120
+
121
+ def tick(self, now_ts=None):
122
+ current_ts = lambda: now_ts if now_ts else time.time()
123
+ current_dt = lambda: (
124
+ legacy.to_datetime(now_ts) if now_ts else legacy.utc_now()
125
+ )
126
+
127
+ timestamp = current_ts() + 5.0
128
+
129
+ while True:
130
+ if not self._tasks:
131
+ break
132
+
133
+ timestamp, task = self._tasks[0]
134
+ if timestamp > current_ts():
135
+ break
136
+
137
+ heapq.heappop(self._tasks)
138
+
139
+ if task.enabled:
140
+ task.job()
141
+ heapq.heappush(
142
+ self._tasks, (task.next_timestamp(now=current_dt()), task)
143
+ )
144
+
145
+ self.timeout = max(0, timestamp - current_ts())
146
+
147
+ def schedule(self, job, cron, now=None):
148
+ """
149
+ Schedules the provided job function for execution according
150
+ to the provided cron string.
151
+
152
+ The optional now parameter may be used to provide the current
153
+ time reference for the scheduling operation, meaning that the next
154
+ timestamp will be calculated using this value as reference.
155
+
156
+ :type job: Function
157
+ :param job: The function to be executed as the job.
158
+ :type cron: String/SchedulerDate
159
+ :param cron: The cron like string defining the schedule.
160
+ :type now: datetime
161
+ :param now: Optional time reference for the job scheduling.
162
+ :rtype: SchedulerTask
163
+ :return: The task object that was created for the job.
164
+ """
165
+
166
+ task = SchedulerTask(job, cron)
167
+ heapq.heappush(self._tasks, (task.next_timestamp(now=now), task))
168
+ self.awake()
169
+ return task
170
+
171
+ def next_run(self):
172
+ timestamp = self.next_timestamp()
173
+ if not timestamp:
174
+ return None
175
+ return legacy.to_datetime(timestamp)
176
+
177
+ def next_timestamp(self):
178
+ if not self._tasks:
179
+ return None
180
+ return self._tasks[0][0]
181
+
182
+
183
+ class SchedulerTask(object):
184
+
185
+ def __init__(self, job, cron):
186
+ self.job = job
187
+ self.date = SchedulerDate.from_cron(cron)
188
+ self._enabled = True
189
+
190
+ def enable(self):
191
+ self._enabled = True
192
+
193
+ def disable(self):
194
+ self._enabled = False
195
+
196
+ def next_run(self, now=None):
197
+ return self.date.next_run(now=now)
198
+
199
+ def next_timestamp(self, now=None):
200
+ return self.date.next_timestamp(now=now)
201
+
202
+ @property
203
+ def enabled(self):
204
+ return self._enabled
205
+
206
+
207
+ class SchedulerDate(object):
208
+
209
+ def __init__(
210
+ self, minutes="*", hours="*", days_of_month="*", months="*", days_of_week="*"
211
+ ):
212
+ self.minutes = self._parse_field(minutes, 0, 59)
213
+ self.hours = self._parse_field(hours, 0, 23)
214
+ self.days_of_month = self._parse_field(days_of_month, 1, 31)
215
+ self.months = self._parse_field(months, 1, 12)
216
+ self.days_of_week = self._parse_field(days_of_week, 0, 6)
217
+
218
+ @classmethod
219
+ def from_cron(cls, cron):
220
+ if isinstance(cron, cls):
221
+ return cron
222
+ values = (value.strip().split(",") for value in cron.split(" "))
223
+ return cls(*values)
224
+
225
+ def next_timestamp(self, now=None):
226
+ date = self.next_run(now=now)
227
+ return legacy.to_timestamp(date)
228
+
229
+ def next_run(self, now=None):
230
+ """
231
+ Calculate the next run time starting from the current time.
232
+ This operation is done respecting Cron rules.
233
+
234
+ :type now: datetime
235
+ :param now: Optional date time to be used as the current time.
236
+ :rtype: datetime
237
+ :return: The next run time respecting Cron rules.
238
+ """
239
+
240
+ now = now or legacy.utc_now()
241
+ now_day = datetime.datetime(now.year, now.month, now.day)
242
+ now_hour = datetime.datetime(now.year, now.month, now.day, hour=now.hour)
243
+ now_minute = datetime.datetime(
244
+ now.year, now.month, now.day, hour=now.hour, minute=now.minute
245
+ )
246
+
247
+ year = now.year
248
+
249
+ while True:
250
+ for month in sorted(self.months):
251
+ if month < now.month and year < now.year:
252
+ continue
253
+
254
+ for day in sorted(self.days_of_month):
255
+ try:
256
+ date = datetime.datetime(year, month, day)
257
+ except ValueError:
258
+ continue
259
+ if self.days_of_week and not date.weekday() in self.days_of_week:
260
+ continue
261
+ if date < now_day:
262
+ continue
263
+
264
+ for hour in sorted(self.hours):
265
+ if date.replace(hour=hour) < now_hour:
266
+ continue
267
+
268
+ for minute in sorted(self.minutes):
269
+ _date = date.replace(
270
+ hour=hour, minute=minute, second=0, microsecond=0
271
+ )
272
+ if _date > now_minute:
273
+ return _date
274
+
275
+ year += 1
276
+
277
+ def _parse_field(self, field, min_value, max_value):
278
+ if field in ("*", ["*"], ("*",)):
279
+ return set(range(min_value, max_value + 1))
280
+ elif isinstance(field, (list, tuple)):
281
+ return set(int(v) for v in field)
282
+ else:
283
+ return set((int(field),))
284
+
285
+
286
+ class Cron(object):
287
+ pass
@@ -275,7 +275,9 @@ class BaseTest(unittest.TestCase):
275
275
  self.assertEqual(type(result), str)
276
276
  self.assertEqual(result, "%e4%bd%a0%e5%a5%bd%e4%b8%96%e7%95%8c")
277
277
 
278
- result = self.app.slugify_slugier(appier.legacy.bytes("你好世界", encoding="utf-8"))
278
+ result = self.app.slugify_slugier(
279
+ appier.legacy.bytes("你好世界", encoding="utf-8")
280
+ )
279
281
  self.assertEqual(type(result), str)
280
282
  self.assertEqual(result, "%e4%bd%a0%e5%a5%bd%e4%b8%96%e7%95%8c")
281
283
 
@@ -385,3 +387,19 @@ class BaseTest(unittest.TestCase):
385
387
  self.assertEqual(result, appier.legacy.u("olá"))
386
388
  result = self.app.template(template, locale="en_us")
387
389
  self.assertEqual(result, appier.legacy.u("hello"))
390
+
391
+ def test_css_abs(self):
392
+ result = appier.base.CSS_ABS_REGEX.sub(
393
+ b"url(http://www.example/\\2)", b"url(image.jpg)"
394
+ )
395
+ self.assertEqual(result, b"url(http://www.example/image.jpg)")
396
+
397
+ result = appier.base.CSS_ABS_REGEX.sub(
398
+ b"url(https://www.example/\\2)", b"url(image.jpg)"
399
+ )
400
+ self.assertEqual(result, b"url(https://www.example/image.jpg)")
401
+
402
+ result = appier.base.CSS_ABS_REGEX.sub(
403
+ b"url(http://www.example/\\2)", b"url(https://example.com/image.jpg)"
404
+ )
405
+ self.assertEqual(result, b"url(https://example.com/image.jpg)")