appier 1.31.5__py2.py3-none-any.whl → 1.33.0__py2.py3-none-any.whl

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.
appier/__init__.py CHANGED
@@ -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,
appier/base.py CHANGED
@@ -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.31.5"
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]
@@ -2332,6 +2362,9 @@ class App(
2332
2362
  receivers=[],
2333
2363
  cc=[],
2334
2364
  bcc=[],
2365
+ reply_to=[],
2366
+ return_path=None,
2367
+ priority=None,
2335
2368
  subject="",
2336
2369
  plain_template=None,
2337
2370
  smtp_url=None,
@@ -2395,6 +2428,8 @@ class App(
2395
2428
  cc = [cc]
2396
2429
  if not isinstance(bcc, (list, tuple)):
2397
2430
  bcc = [bcc]
2431
+ if not isinstance(reply_to, (list, tuple)):
2432
+ reply_to = [reply_to]
2398
2433
 
2399
2434
  sender_base = util.email_base(sender)
2400
2435
  receivers_base = util.email_base(receivers)
@@ -2406,6 +2441,7 @@ class App(
2406
2441
  sender_mime = util.email_mime(sender)
2407
2442
  receivers_mime = util.email_mime(receivers)
2408
2443
  cc_mime = util.email_mime(cc)
2444
+ reply_to_mime = util.email_mime(reply_to)
2409
2445
 
2410
2446
  parameters = dict(kwargs)
2411
2447
  parameters.update(
@@ -2440,6 +2476,12 @@ class App(
2440
2476
  )
2441
2477
  if cc_mime:
2442
2478
  mime["Cc"] = ", ".join(cc_mime)
2479
+ if reply_to_mime:
2480
+ mime["Reply-To"] = ", ".join(reply_to_mime)
2481
+ if return_path:
2482
+ mime["Return-Path"] = return_path
2483
+ if priority:
2484
+ mime["Priority"] = priority
2443
2485
 
2444
2486
  for key, value in headers.items():
2445
2487
  mime[key] = value
@@ -2606,7 +2648,7 @@ class App(
2606
2648
  self.template_args(kwargs)
2607
2649
 
2608
2650
  # verifies if the target locale for the template has been defined
2609
- # 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
2610
2652
  # the current template render to include that value
2611
2653
  if locale:
2612
2654
  kwargs["_locale"] = locale
@@ -3751,8 +3793,8 @@ class App(
3751
3793
  timeout = int(max_age)
3752
3794
 
3753
3795
  # in case the type of the resource is css an extra replace operation
3754
- # on the urls must be performed so that the base URL is added to all
3755
- # 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
3756
3798
  if type == "css":
3757
3799
  base, _name = url.rsplit("/", 1)
3758
3800
  base = legacy.bytes(base)
@@ -5344,6 +5386,16 @@ class App(
5344
5386
  def _stop_supervisor(self):
5345
5387
  pass
5346
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
+
5347
5399
  def _add_route(self, *args, **kwargs):
5348
5400
  self.__routes.append(args)
5349
5401
  self.clear_routes()
appier/legacy.py CHANGED
@@ -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
 
appier/model.py CHANGED
@@ -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
  }
appier/scheduler.py CHANGED
@@ -28,11 +28,16 @@ __copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda."
28
28
  __license__ = "Apache License, Version 2.0"
29
29
  """ The license for the module """
30
30
 
31
+ import time
32
+ import heapq
33
+ import calendar
34
+ import datetime
31
35
  import logging
32
36
  import threading
33
37
  import traceback
34
38
 
35
39
  from . import config
40
+ from . import legacy
36
41
 
37
42
  LOOP_TIMEOUT = 60.0
38
43
  """ The time value to be used to sleep the main sequence
@@ -43,11 +48,14 @@ of time between external interactions """
43
48
 
44
49
  class Scheduler(threading.Thread):
45
50
  """
46
- Scheduler class that handles all the async tasks
47
- related with the house keeping of the appier
48
- infra-structure. The architecture of the logic
49
- for the class should be modular in the sense that
50
- new task may be added to it through a queue system.
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.
51
59
  """
52
60
 
53
61
  def __init__(self, owner, timeout=LOOP_TIMEOUT, daemon=True):
@@ -73,8 +81,10 @@ class Scheduler(threading.Thread):
73
81
  self._condition.wait(self.timeout)
74
82
  self._condition.release()
75
83
 
76
- def stop(self):
84
+ def stop(self, awake=True):
77
85
  self.running = False
86
+ if awake:
87
+ self.awake()
78
88
 
79
89
  def tick(self):
80
90
  pass
@@ -93,3 +103,185 @@ class Scheduler(threading.Thread):
93
103
  return self.owner.logger
94
104
  else:
95
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
appier/test/base.py CHANGED
@@ -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)")
@@ -0,0 +1,173 @@
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 calendar
32
+ import datetime
33
+ import unittest
34
+
35
+ import appier
36
+
37
+
38
+ class CronSchedulerTest(unittest.TestCase):
39
+
40
+ def test_basic(self):
41
+ state = dict(value=0)
42
+
43
+ def increment():
44
+ state["value"] += 1
45
+
46
+ scheduler = appier.CronScheduler(None)
47
+ task = scheduler.schedule(
48
+ lambda: increment(),
49
+ "11",
50
+ now=datetime.datetime(2013, 1, 1, hour=1, minute=1),
51
+ )
52
+ self.assertNotEqual(task, None)
53
+ self.assertEqual(isinstance(task, appier.SchedulerTask), True)
54
+ self.assertEqual(task.enabled, True)
55
+ self.assertEqual(
56
+ scheduler.next_run(), datetime.datetime(2013, 1, 1, hour=1, minute=11)
57
+ )
58
+
59
+ scheduler.tick(
60
+ now_ts=calendar.timegm(
61
+ datetime.datetime(2013, 1, 1, hour=1, minute=1).utctimetuple()
62
+ )
63
+ )
64
+ self.assertEqual(state["value"], 0)
65
+ self.assertEqual(scheduler.timeout, 600)
66
+
67
+ scheduler.tick(
68
+ now_ts=calendar.timegm(
69
+ datetime.datetime(2013, 1, 1, hour=1, minute=11).utctimetuple()
70
+ )
71
+ )
72
+ self.assertEqual(state["value"], 1)
73
+ self.assertEqual(scheduler.timeout, 3600)
74
+
75
+ scheduler.tick(
76
+ now_ts=calendar.timegm(
77
+ datetime.datetime(2013, 1, 1, hour=2, minute=11).utctimetuple()
78
+ )
79
+ )
80
+ self.assertEqual(state["value"], 2)
81
+ self.assertEqual(scheduler.timeout, 3600)
82
+
83
+ def test_scheduler_date(self):
84
+ state = dict(value=0)
85
+
86
+ def increment():
87
+ state["value"] += 1
88
+
89
+ scheduler = appier.CronScheduler(None)
90
+ task = scheduler.schedule(
91
+ lambda: increment(),
92
+ appier.SchedulerDate(minutes=11),
93
+ now=datetime.datetime(2013, 1, 1, hour=1, minute=1),
94
+ )
95
+ self.assertNotEqual(task, None)
96
+ self.assertEqual(isinstance(task, appier.SchedulerTask), True)
97
+ self.assertEqual(task.enabled, True)
98
+ self.assertEqual(
99
+ scheduler.next_run(), datetime.datetime(2013, 1, 1, hour=1, minute=11)
100
+ )
101
+
102
+ scheduler.tick(
103
+ now_ts=calendar.timegm(
104
+ datetime.datetime(2013, 1, 1, hour=1, minute=1).utctimetuple()
105
+ )
106
+ )
107
+ self.assertEqual(state["value"], 0)
108
+ self.assertEqual(scheduler.timeout, 600)
109
+
110
+ scheduler.tick(
111
+ now_ts=calendar.timegm(
112
+ datetime.datetime(2013, 1, 1, hour=1, minute=11).utctimetuple()
113
+ )
114
+ )
115
+ self.assertEqual(state["value"], 1)
116
+ self.assertEqual(scheduler.timeout, 3600)
117
+
118
+ def test_week_days(self):
119
+ state = dict(value=0)
120
+
121
+ def increment():
122
+ state["value"] += 1
123
+
124
+ scheduler = appier.CronScheduler(None)
125
+ task = scheduler.schedule(
126
+ lambda: increment(),
127
+ appier.SchedulerDate(minutes=11, days_of_month=10, days_of_week=2),
128
+ now=datetime.datetime(2013, 1, 1, hour=1, minute=1),
129
+ )
130
+ self.assertNotEqual(task, None)
131
+ self.assertEqual(isinstance(task, appier.SchedulerTask), True)
132
+ self.assertEqual(task.enabled, True)
133
+ self.assertEqual(
134
+ scheduler.next_run(), datetime.datetime(2013, 4, 10, hour=0, minute=11)
135
+ )
136
+
137
+
138
+ class SchedulerDateTest(unittest.TestCase):
139
+
140
+ def test_from_cron(self):
141
+ date = appier.SchedulerDate.from_cron("11")
142
+ self.assertEqual(date.minutes, set((11,)))
143
+ self.assertEqual(date.hours, set(range(0, 24)))
144
+ self.assertEqual(date.days_of_month, set(range(1, 32)))
145
+ self.assertEqual(date.months, set(range(1, 13)))
146
+ self.assertEqual(date.days_of_week, set(range(0, 7)))
147
+
148
+ def test_next_run(self):
149
+ date = appier.SchedulerDate.from_cron("11")
150
+
151
+ value = date.next_run(now=datetime.datetime(2013, 1, 1, hour=1, minute=1))
152
+ self.assertEqual(value, datetime.datetime(2013, 1, 1, hour=1, minute=11))
153
+
154
+ value = date.next_run(now=datetime.datetime(2013, 1, 1, hour=1, minute=12))
155
+ self.assertEqual(value, datetime.datetime(2013, 1, 1, hour=2, minute=11))
156
+
157
+ value = date.next_run(now=datetime.datetime(2013, 12, 31, hour=23, minute=12))
158
+ self.assertEqual(value, datetime.datetime(2014, 1, 1, hour=0, minute=11))
159
+
160
+ def test_next_run_complex(self):
161
+ date = appier.SchedulerDate.from_cron("11 3 10,16,20")
162
+ value = date.next_run(now=datetime.datetime(2013, 1, 1, hour=1, minute=1))
163
+ self.assertEqual(value, datetime.datetime(2013, 1, 10, hour=3, minute=11))
164
+
165
+ value = date.next_run(now=datetime.datetime(2013, 1, 10, hour=4, minute=1))
166
+ self.assertEqual(value, datetime.datetime(2013, 1, 16, hour=3, minute=11))
167
+
168
+ date = appier.SchedulerDate.from_cron("* 3 10,16,20")
169
+ value = date.next_run(now=datetime.datetime(2013, 1, 10, hour=3, minute=1))
170
+ self.assertEqual(value, datetime.datetime(2013, 1, 10, hour=3, minute=2))
171
+
172
+ value = date.next_run(now=datetime.datetime(2013, 1, 10, hour=3, minute=2))
173
+ self.assertEqual(value, datetime.datetime(2013, 1, 10, hour=3, minute=3))
appier/test/typesf.py CHANGED
@@ -219,7 +219,7 @@ class TypesfTest(unittest.TestCase):
219
219
  elif isinstance(value, datetime.datetime):
220
220
  self._datetime = value
221
221
  elif isinstance(value, (int, float)):
222
- self._datetime = datetime.datetime.utcfromtimestamp(value)
222
+ self._datetime = appier.legacy.utcfromtimestamp(value)
223
223
  else:
224
224
  raise appier.OperationalError()
225
225
 
@@ -0,0 +1,119 @@
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 unittest
32
+
33
+ import appier
34
+
35
+
36
+ class ValidationTest(unittest.TestCase):
37
+
38
+ def test_is_simple(self):
39
+ self.assertEqual(appier.is_simple("value")(dict(value=""), None), True)
40
+ self.assertEqual(appier.is_simple("value")(dict(value="hello"), None), True)
41
+ self.assertEqual(
42
+ appier.is_simple("value")(dict(value="hello world"), None), True
43
+ )
44
+
45
+ with self.assertRaises(appier.ValidationInternalError):
46
+ self.assertEqual(
47
+ appier.is_simple("value")(dict(value="hello?world"), None), True
48
+ )
49
+ with self.assertRaises(appier.ValidationInternalError):
50
+ self.assertEqual(
51
+ appier.is_simple("value")(dict(value="hello=world"), None), True
52
+ )
53
+ with self.assertRaises(appier.ValidationInternalError):
54
+ self.assertEqual(
55
+ appier.is_simple("value")(
56
+ dict(value=appier.legacy.u("你好世界")), None
57
+ ),
58
+ True,
59
+ )
60
+
61
+ def test_is_email(self):
62
+ self.assertEqual(appier.is_email("value")(dict(value=""), None), True)
63
+ self.assertEqual(
64
+ appier.is_email("value")(dict(value="user@domain.com"), None), True
65
+ )
66
+ self.assertEqual(
67
+ appier.is_email("value")(dict(value="first.second@domain.com"), None), True
68
+ )
69
+ self.assertEqual(
70
+ appier.is_email("value")(dict(value="first+second@domain.com"), None), True
71
+ )
72
+ self.assertEqual(
73
+ appier.is_email("value")(
74
+ dict(value=appier.legacy.u("你好世界@domain.com")), None
75
+ ),
76
+ True,
77
+ )
78
+
79
+ with self.assertRaises(appier.ValidationInternalError):
80
+ appier.is_email("value")(dict(value="user"), None)
81
+ with self.assertRaises(appier.ValidationInternalError):
82
+ appier.is_email("value")(dict(value="domain"), None)
83
+ with self.assertRaises(appier.ValidationInternalError):
84
+ appier.is_email("value")(dict(value="domain.com"), None)
85
+ with self.assertRaises(appier.ValidationInternalError):
86
+ appier.is_email("value")(dict(value="user@"), None)
87
+ with self.assertRaises(appier.ValidationInternalError):
88
+ appier.is_email("value")(dict(value=appier.legacy.u("你好世界")), None)
89
+ with self.assertRaises(appier.ValidationInternalError):
90
+ appier.is_email("value")(dict(value=appier.legacy.u("你好世界@")), None)
91
+
92
+ def test_is_url(self):
93
+ self.assertEqual(appier.is_url("value")(dict(value=""), None), True)
94
+ self.assertEqual(
95
+ appier.is_url("value")(dict(value="http://hello.com"), None), True
96
+ )
97
+ self.assertEqual(
98
+ appier.is_url("value")(dict(value="http://hello.world"), None), True
99
+ )
100
+ self.assertEqual(
101
+ appier.is_url("value")(dict(value="https://hello.world"), None), True
102
+ )
103
+ self.assertEqual(
104
+ appier.is_url("value")(
105
+ dict(value=appier.legacy.u("https://你好世界")), None
106
+ ),
107
+ True,
108
+ )
109
+ self.assertEqual(appier.is_url("value")(dict(value="http://hello"), None), True)
110
+ self.assertEqual(
111
+ appier.is_url("value")(dict(value="mailto://hello"), None), True
112
+ )
113
+
114
+ with self.assertRaises(appier.ValidationInternalError):
115
+ appier.is_email("value")(dict(value=appier.legacy.u("http:")), None)
116
+ with self.assertRaises(appier.ValidationInternalError):
117
+ appier.is_email("value")(dict(value=appier.legacy.u("http://")), None)
118
+ with self.assertRaises(appier.ValidationInternalError):
119
+ appier.is_email("value")(dict(value=appier.legacy.u("mailto://")), None)
appier/typesf.py CHANGED
@@ -615,13 +615,12 @@ def image(width=None, height=None, format="png", **kwargs):
615
615
 
616
616
  # resizes the already cropped image into the target size using an
617
617
  # anti alias based algorithm (default expectations)
618
- default_resample = (
619
- PIL.Image.ANTIALIAS # type: ignore
620
- if hasattr(PIL.Image, "ANTIALIAS")
621
- else (PIL.Image.LANCZOS if hasattr(PIL.Image, "LANCZOS") else None) # type: ignore
622
- )
623
618
  if resample == None:
624
- resample = default_resample
619
+ resample = (
620
+ PIL.Image.ANTIALIAS # type: ignore
621
+ if hasattr(PIL.Image, "ANTIALIAS")
622
+ else (PIL.Image.LANCZOS if hasattr(PIL.Image, "LANCZOS") else None) # type: ignore
623
+ )
625
624
  image = image.resize(size, resample)
626
625
  return image
627
626
 
appier/util.py CHANGED
@@ -59,11 +59,11 @@ CREATION_COUNTER = 0
59
59
  will be used to create an order in the declaration
60
60
  of attributes for a set of classes """
61
61
 
62
- FIRST_CAP_REGEX = re.compile("(.)([A-Z][a-z]+)")
62
+ FIRST_CAP_REGEX = re.compile(r"(.)([A-Z][a-z]+)")
63
63
  """ Regular expression that ensures that the first
64
64
  token of each camel string is properly capitalized """
65
65
 
66
- ALL_CAP_REGEX = re.compile("([a-z0-9])([A-Z])")
66
+ ALL_CAP_REGEX = re.compile(r"([a-z0-9])([A-Z])")
67
67
  """ The generalized transition from lower case to
68
68
  upper case letter regex that will provide a way of
69
69
  putting the underscore in the middle of the transition """
@@ -1110,7 +1110,7 @@ def escape(value, char, escape="\\"):
1110
1110
  :param char: The character(s) that is going to be "target" of escaping
1111
1111
  or a list of characters for escaping.
1112
1112
  :type escape: String
1113
- :param escape: The character to be used for escaping (typically `\`).
1113
+ :param escape: The character to be used for escaping (typically '\').
1114
1114
  :rtype: String
1115
1115
  :return: The final string with the target character properly escaped.
1116
1116
  """
appier/validation.py CHANGED
@@ -37,31 +37,31 @@ from . import common
37
37
  from . import legacy
38
38
  from . import exceptions
39
39
 
40
- SIMPLE_REGEX_VALUE = "^[\:\.\s\w-]+$"
40
+ SIMPLE_REGEX_VALUE = r"^[\:\.\s\w-]+$"
41
41
  """ The simple regex value used to validate
42
42
  if the provided value is a "simple" one meaning
43
43
  that it may be used safely for URL parts """
44
44
 
45
- EMAIL_REGEX_VALUE = "^[\w\d\._%+-]+@[\w\d\.\-]+$"
45
+ EMAIL_REGEX_VALUE = r"^[\w\d\._%+-]+@[\w\d\.\-]+$"
46
46
  """ The email regex value used to validate
47
47
  if the provided value is in fact an email """
48
48
 
49
49
  URL_REGEX_VALUE = (
50
- "^\w+\:\/\/([^@]+\:[^@]+@)?[^\:\/\?#]+(\:\d+)?(\/[^\?#]+)*\/?(\?[^#]*)?(#.*)?$"
50
+ r"^\w+\:\/\/([^@]+\:[^@]+@)?[^\:\/\?#]+(\:\d+)?(\/[^\?#]+)*\/?(\?[^#]*)?(#.*)?$"
51
51
  )
52
52
  """ The URL regex value used to validate
53
53
  if the provided value is in fact an URL/URI """
54
54
 
55
- SIMPLE_REGEX = re.compile(SIMPLE_REGEX_VALUE)
55
+ SIMPLE_REGEX = re.compile(SIMPLE_REGEX_VALUE, re.ASCII if hasattr(re, "ASCII") else 0)
56
56
  """ The simple regex used to validate
57
57
  if the provided value is a "simple" one meaning
58
58
  that it may be used safely for URL parts """
59
59
 
60
- EMAIL_REGEX = re.compile(EMAIL_REGEX_VALUE)
60
+ EMAIL_REGEX = re.compile(EMAIL_REGEX_VALUE, re.UNICODE)
61
61
  """ The email regex used to validate
62
62
  if the provided value is in fact an email """
63
63
 
64
- URL_REGEX = re.compile(URL_REGEX_VALUE)
64
+ URL_REGEX = re.compile(URL_REGEX_VALUE, re.UNICODE)
65
65
  """ The URL regex used to validate
66
66
  if the provided value is in fact an URL/URI """
67
67
 
@@ -144,9 +144,9 @@ def validate_e(method=None, methods=[], object=None, ctx=None, build=True):
144
144
  raise exceptions.ValidationError(errors_map, object)
145
145
 
146
146
 
147
- def safe(comparision):
147
+ def safe(comparison):
148
148
  try:
149
- return comparision()
149
+ return comparison()
150
150
  except TypeError:
151
151
  return False
152
152
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: appier
3
- Version: 1.31.5
3
+ Version: 1.33.0
4
4
  Summary: Appier Framework
5
5
  Home-page: http://appier.hive.pt
6
6
  Author: Hive Solutions Lda.
@@ -1,11 +1,11 @@
1
- appier/__init__.py,sha256=guj5XljPfdUVWWwLu_MUGyEWydsPqFVOcBex95oh5UM,9304
1
+ appier/__init__.py,sha256=2UqTnGyy5JGVUsLXxRzfr7br1oKmY6wpwgf0bH89dgM,9355
2
2
  appier/amqp.py,sha256=etYxUlfaK27Og_9FJ6qCgNLSYhnz9XgVhIhSmD2ITW4,3852
3
3
  appier/api.py,sha256=s5ZJs0tgR3wktAhLZ7Vuhbd_4lWPsGJSEx0ptkqLxvM,14369
4
4
  appier/asgi.py,sha256=sKQH9K_QEDPbBSk3AWcvHo7Lfl5yawIo6NcM0z1qQCw,12100
5
5
  appier/async_neo.py,sha256=gQpyT1-nZE6Uynh5FEmFl5kfXkxChdUsgiKgwlaNw5E,5446
6
6
  appier/async_old.py,sha256=m3BFqHVPCRuIZ9L2sGYhCQPEwuCNKBO2y7SKM0dbtj8,9194
7
7
  appier/asynchronous.py,sha256=a1LQa3wbGMaXELhF7W71dRr1klPOj6x-ST9EInvPhtU,1757
8
- appier/base.py,sha256=QwZSi5dqt1SKBCNnA1sq7YvmluBbXerjtNBM0a7wMWo,267288
8
+ appier/base.py,sha256=CSPjeAmoawPDoKCPWv8z5UiyCbskHa7sg7jU7FwvWHU,268944
9
9
  appier/bus.py,sha256=AMC2UXlaf6rzhzlIyTHkzbfb6tdfPBiOii4lpl9AAwg,7670
10
10
  appier/cache.py,sha256=V1nhf4aHJpUlcFtxVsd-2tGCt1BarkxvHWwVwuhc4Ao,10689
11
11
  appier/common.py,sha256=fcECBvu-KcB9DImZzkpM_FA_PUqgCx0FKP4KHuOikYc,1313
@@ -26,11 +26,11 @@ appier/geo.py,sha256=EEPh8oX7Uk7iert-XKt7HCw0swicc8lFAv7XFeQSbZ8,4877
26
26
  appier/git.py,sha256=-neM2mxqlyiCNu17CpLyiz3RN7Ti6LtAx5L1tipxAGA,9785
27
27
  appier/graph.py,sha256=jSgu-ookILujLm4ivW14SymVmuI1nbjbVZMgRS2ehOg,4054
28
28
  appier/http.py,sha256=Xk871N8yCN39OGkm5WqQ5M9lnI9teG9t9_-A5zdVwy4,38635
29
- appier/legacy.py,sha256=ErZDOOOdakTyKylqdfhoqKp0JOtXp5lXdOpP7HGZxZE,14974
29
+ appier/legacy.py,sha256=2ldRfzwQg6SQXp9dC8q7pl6-aNBAQ0AdeF_2rllcYeE,15881
30
30
  appier/log.py,sha256=QJR4lLkVe6olbsY9lrVB78-1WGMxATzLA-Y9slE_XIk,12772
31
31
  appier/meta.py,sha256=rgBLOjD6QU9CGYsbCQS3Fy4iY14uk1-Kd8ljkfmxxzc,7168
32
32
  appier/mock.py,sha256=WoWa67rb8qV_ogToQJCdT0R-rCw9RUY24EkA4bYR1G4,5800
33
- appier/model.py,sha256=gibqIPOBAYfW-A0OQgir1OSU8OLIgS6ozhZyElGvh6s,121756
33
+ appier/model.py,sha256=S3Qn5R36EKZFomf52A49Qmf3oEVwA0ciP_BSqSrOGSc,121790
34
34
  appier/model_a.py,sha256=c6XpG4oIelXNDK0uicsZ69-f6isUgmh5-29F61PZt9c,16176
35
35
  appier/mongo.py,sha256=t-257ReqFWnEw0bQkDvS4McsPsc6AuKoQXIFHxlrOMw,10891
36
36
  appier/observer.py,sha256=T0QpkxdkcNuEOZJdmJT5nFSJ2e-0VPbGveX5YKtt7mA,4519
@@ -39,16 +39,16 @@ appier/preferences.py,sha256=3JrH2f8Drzw0zNTDA4MtOYMzhUaBm3Rr4d6E95mL49o,8287
39
39
  appier/queuing.py,sha256=6AJlRVdd5JunzChu8Q1U-9Biq4vQUHTuJFC8qs2wzCs,8097
40
40
  appier/redisdb.py,sha256=5inJMQqHVkfsGoqoRGYzewlgooDZD2uwOtlPXHX17V0,3504
41
41
  appier/request.py,sha256=vNiRdBZXgboMx_d3xkGHlJf3f7ifejgKyhYZ4Pr9GlE,31450
42
- appier/scheduler.py,sha256=pKAU_Lsn5n3GCWWXwABg1wFWFA1C7LfjmeK4D0lC35k,3075
42
+ appier/scheduler.py,sha256=5BRlH5jjs594wRqpZcwlNOFSbl9cML2YdY7OKAWCtzQ,9228
43
43
  appier/serialize.py,sha256=LiR1ZbAiC_I3lhlQToPK_u2DC2r04Gl9OEQTayW4Rlg,6669
44
44
  appier/session.py,sha256=6rcwFdsu--7gFLr6mJJh3EFSZ_A3Oe8u7olAvTw2sUk,20380
45
45
  appier/settings.py,sha256=28tTRJIZ47iwXqUs6YtUoW7ntEVsp_0-QfONVDGA1sg,1136
46
46
  appier/smtp.py,sha256=5tW0dOEm03kejLoQyhRQFjgjYRd-jhb-VlhEKDb4Zn0,3434
47
47
  appier/storage.py,sha256=Qp_CMCDg85q35n9lggNVBZRWwlGrQ-nLYNj0W7Mnbec,7470
48
48
  appier/structures.py,sha256=YSv0YwZk8tRjPn9N6YEtYoPQ72PZQkfLEtTI_H6jXV8,8076
49
- appier/typesf.py,sha256=nNhMblrZDnaM1A9Lk4wGYsMq0r55dJCCh96MJR_tQSI,36915
50
- appier/util.py,sha256=9ojlLWylDB22YfDMNmmzrykdfqR2HPpA2vGIQBzR4Wo,86774
51
- appier/validation.py,sha256=vipgF1oFmFhbSfZXC2lhHpox_nburIa6VIFs2Du2mhc,22322
49
+ appier/typesf.py,sha256=iby3f-3XayIOu9FgjHS6I9L3UoaJntHtG73eKXgIgFE,36882
50
+ appier/util.py,sha256=L9ea0WTmvcelkgKSztHXgQNRhlireKcrOlXpkgrrkRo,86776
51
+ appier/validation.py,sha256=vxb289skd7RfGEQVmpkQxYb71JdAfLu17QaKOuAAEEQ,22388
52
52
  appier/res/static/css/base.css,sha256=J9zLozd57KoslAsrsj2a42glGTObxbkrrckR-W-_f2A,6127
53
53
  appier/res/static/images/favicon.ico,sha256=fAL8DLx_0Yl6jwpRG_K6S7g3yXxr9Hyjn3JmqyU8t7U,1150
54
54
  appier/res/static/js/base.js,sha256=8o0rvcqF3O2gfPTQgna2ni56IBbr50xCYU8Eq-LSRQI,4953
@@ -56,7 +56,7 @@ appier/res/templates/error.html.tpl,sha256=CiKD-gCC1Zed0zfPFEo6a8qqrEz8Ye9LT-L45
56
56
  appier/res/templates/holder.html.tpl,sha256=lV-z0796VhGcowE9rG9MziHbu2S4H1uxfPmqsbx3ddA,1076
57
57
  appier/res/templates/layout.html.tpl,sha256=e6CVkHTtF8zlFClKFBVWJgMZQ1paHi_240pXahH73BY,2183
58
58
  appier/test/__init__.py,sha256=zMHH-X-dnws-iNXL4JudfPNz1h5oeRSXeS5O5TjoyZk,990
59
- appier/test/base.py,sha256=X4PXSmZddrHP2UODIPkKSnhjwFu2jORbHgfC43Ftd0k,14190
59
+ appier/test/base.py,sha256=dJyWcr8egJkrjw4_p9LkUTi9GsFpL82i3lu0nJGrzcc,14857
60
60
  appier/test/cache.py,sha256=WpbfgdsXfAN9Zwo0MZvATygdl-03yqsDYAWKfn4gQf0,6922
61
61
  appier/test/config.py,sha256=ured3diixEYrfGEbwNP89MkCkBbYepWEmxB4Nkuraeo,2358
62
62
  appier/test/crypt.py,sha256=wXIHJjKZJe9qhKfIeGMz5PvQWYRIQVnwbKW-dwUukAo,2457
@@ -73,14 +73,16 @@ appier/test/part.py,sha256=yHQxg2hdfojJMO2Fs0NLvYTX3uCJ6xwgsD4VaKV04ak,3352
73
73
  appier/test/preferences.py,sha256=QyqoBddOCAmhkcgUYAJ5RgypqrkGL6x6s6a0jsIwE9w,2890
74
74
  appier/test/queuing.py,sha256=Lq7gI5QkZfXPf_MUzmRd6LHHknf9EryF-MauWjDLJn0,5710
75
75
  appier/test/request.py,sha256=h3DdvhEMARpYTp7eRcJ3-qLQyG-a8PHqSnWY52BiOeU,8176
76
+ appier/test/scheduler.py,sha256=Y2r91OQ-bt5bzkT1x5otdK2k4RO77c9oLgA5oZ-8Hbg,6360
76
77
  appier/test/serialize.py,sha256=roX01n86AQfnPxzPVdIjdIUQWC_x0C_HE3hNgF37ci8,2120
77
78
  appier/test/session.py,sha256=KdiYLLB5autIEu1sHwOuYJXVd0y6RMPgg0ITBuRTMfA,4419
78
79
  appier/test/smtp.py,sha256=XJNa0ZTmabdrX8MfVMgcqBxabvBfHgIimvozdziP4_E,1826
79
80
  appier/test/structures.py,sha256=Uylzx5vLNORXKG5wsPMdIreW1SlHWGnmSQouqQNG6E8,6745
80
- appier/test/typesf.py,sha256=td-e4mqTO_SYH7txzF5au_ZMNQ6kQsFymSjGCiVc1bU,9684
81
+ appier/test/typesf.py,sha256=KHumQFzx7wPZSCb8_mpIwobhIy2Fh_0XYviwPjXMbKI,9680
81
82
  appier/test/util.py,sha256=SSHuBAZQ0TcXLDYI0xUvmwId5nndDZb88s_5Y-Hqibw,46884
82
- appier-1.31.5.dist-info/LICENSE,sha256=Pd-b5cKP4n2tFDpdx27qJSIq0d1ok0oEcGTlbtL6QMU,11560
83
- appier-1.31.5.dist-info/METADATA,sha256=Z7rcsfIDdDWY4IarL4eX653g0tsdxpvkcCROUaK4XGU,1920
84
- appier-1.31.5.dist-info/WHEEL,sha256=kGT74LWyRUZrL4VgLh6_g12IeVl_9u9ZVhadrgXZUEY,110
85
- appier-1.31.5.dist-info/top_level.txt,sha256=Z2e_Y1ya06a554WwQZkfNRiaaQxqsdaPtBzrck384Lo,7
86
- appier-1.31.5.dist-info/RECORD,,
83
+ appier/test/validation.py,sha256=riOCsGKob1P5jnbcB5qGZ45ApimNAVS0byg9v_uUdrk,4952
84
+ appier-1.33.0.dist-info/LICENSE,sha256=Pd-b5cKP4n2tFDpdx27qJSIq0d1ok0oEcGTlbtL6QMU,11560
85
+ appier-1.33.0.dist-info/METADATA,sha256=15fdtmGnRjQCiRtcnImCg5krRWYsZ_2bCGr5PGxyhvI,1920
86
+ appier-1.33.0.dist-info/WHEEL,sha256=kGT74LWyRUZrL4VgLh6_g12IeVl_9u9ZVhadrgXZUEY,110
87
+ appier-1.33.0.dist-info/top_level.txt,sha256=Z2e_Y1ya06a554WwQZkfNRiaaQxqsdaPtBzrck384Lo,7
88
+ appier-1.33.0.dist-info/RECORD,,