future-framework 3.0.2__tar.gz → 3.1.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.
- {future_framework-3.0.2 → future_framework-3.1.0}/PKG-INFO +25 -32
- {future_framework-3.0.2 → future_framework-3.1.0}/README.md +3 -3
- {future_framework-3.0.2 → future_framework-3.1.0}/future/cli.py +17 -18
- {future_framework-3.0.2 → future_framework-3.1.0}/future/interfaces/ITask.py +2 -1
- future_framework-3.1.0/future/scheduling.py +73 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/taskscheduler.py +31 -17
- future_framework-3.1.0/pyproject.toml +127 -0
- future_framework-3.0.2/pyproject.toml → future_framework-3.1.0/pyproject.toml.orig +50 -50
- {future_framework-3.0.2 → future_framework-3.1.0}/LICENSE +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/__init__.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/application.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/Auth0Authentication.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/AzureADAuthentication.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/BasicAuthentication.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/KerberosAuthentication.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/KeycloakAuthentication.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/OAuth2Authentication.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/OpenIdConnectAuthentication.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/SAMLAuthentication.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/__init__.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/controllers/DebugController.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/controllers/GraphQLController.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/controllers/OpenAPIController.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/controllers/WebSocketController.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/controllers/WelcomeController.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/controllers/__init__.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/database.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/databases/ClickhouseDatabase.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/databases/ElasticsearchDatabase.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/databases/MongoDBDatabase.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/databases/MySQLDatabase.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/databases/PostgresDatabase.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/databases/RedisDatabase.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/databases/SQLiteDatabase.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/databases/__init__.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/exceptions.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/interfaces/IAuthentication.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/interfaces/IController.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/interfaces/IDatabase.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/interfaces/IMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/interfaces/IModel.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/interfaces/IPlugin.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/interfaces/__init__.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/interfacing.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/lifespan.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/logger.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/BruteforcePreventionMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/CORSMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/CSRFMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/GZipMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/HTAccessConfuserMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/HeaderConfuserMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/RateLimitMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/ResponseCodeConfuserMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/SQLiConfuserMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/ScopeValidationMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/SessionMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/TestMiddlewareRequest.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/TestMiddlewareResponse.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/WebServerConfuserMiddleware.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/__init__.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/migrations.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/models/PostModel.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/models/Query.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/models/UserModel.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/models/__init__.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/openapi.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/plugins/ElasticsearchPlugin.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/plugins/__init__.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/request.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/response.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/routing.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/seeder.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/settings.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/tasks/CheckDNSTask.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/tasks/CheckDiskUsageTask.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/tasks/CheckHttpStatusTask.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/tasks/CheckMemoryUsageTask.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/tasks/CheckSSHBannerTask.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/tasks/CheckSystemUptimeTask.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/tasks/DailyBackupTask.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/tasks/SyncTaskExample.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/tasks/__init__.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/testclient.py +0 -0
- {future_framework-3.0.2 → future_framework-3.1.0}/future/types.py +0 -0
|
@@ -1,37 +1,31 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: future-framework
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: ASGI-based framework for web APIs
|
|
5
|
-
License: LICENSE
|
|
6
|
-
License-File: LICENSE
|
|
7
5
|
Author: nicolaipre
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Requires-Dist:
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
Requires-Dist:
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist:
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-
|
|
27
|
-
Requires-Dist: rich (>=15.0.0,<16.0.0)
|
|
28
|
-
Requires-Dist: sqlalchemy (>=2.0.51,<3.0.0)
|
|
29
|
-
Requires-Dist: strawberry-graphql (>=0.323.0,<0.324.0)
|
|
30
|
-
Requires-Dist: uvicorn (>=0.51.0,<0.52.0)
|
|
31
|
-
Project-URL: Bug Tracker, https://github.com/nicolaipre/future-framework/issues
|
|
32
|
-
Project-URL: Documentation, https://nicolaipre.github.io/future-framework/
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: uvicorn>=0.51.0,<0.52.0
|
|
9
|
+
Requires-Dist: httpx>=0.28.1,<0.29.0
|
|
10
|
+
Requires-Dist: rich>=15.0.0,<16.0.0
|
|
11
|
+
Requires-Dist: elasticsearch>=8.15.0,<9.0.0
|
|
12
|
+
Requires-Dist: strawberry-graphql>=0.323.0,<0.324.0
|
|
13
|
+
Requires-Dist: python-dotenv>=1.2.2,<2.0.0
|
|
14
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.51,<3.0.0
|
|
15
|
+
Requires-Dist: inflection>=0.5.1,<0.6.0
|
|
16
|
+
Requires-Dist: pymongo>=4.17.0,<5.0.0
|
|
17
|
+
Requires-Dist: aiosqlite>=0.21.0,<0.22.0
|
|
18
|
+
Requires-Dist: aiomysql>=0.2.0,<0.3.0
|
|
19
|
+
Requires-Dist: asyncpg>=0.30.0,<0.31.0
|
|
20
|
+
Requires-Dist: asynch>=0.2.5,<0.3.0
|
|
21
|
+
Requires-Dist: clickhouse-driver>=0.2.11,<0.3.0
|
|
22
|
+
Requires-Dist: redis>=8.0.0,<9.0.0
|
|
23
|
+
Requires-Dist: faker>=40.0.0,<41.0.0
|
|
24
|
+
Requires-Python: >=3.12, <4.0
|
|
33
25
|
Project-URL: Homepage, https://github.com/nicolaipre/future-framework
|
|
34
26
|
Project-URL: Repository, https://github.com/nicolaipre/future-framework
|
|
27
|
+
Project-URL: Documentation, https://nicolaipre.github.io/future-framework/
|
|
28
|
+
Project-URL: Bug Tracker, https://github.com/nicolaipre/future-framework/issues
|
|
35
29
|
Description-Content-Type: text/markdown
|
|
36
30
|
|
|
37
31
|
# Future
|
|
@@ -42,12 +36,12 @@ Description-Content-Type: text/markdown
|
|
|
42
36
|
[](https://codecov.io/gh/nicolaipre/future-framework)
|
|
43
37
|
|
|
44
38
|
## Documentation
|
|
45
|
-
Published at **[nicolaipre.github.io/future-framework](https://nicolaipre.github.io/future-framework/)**. To preview locally: `
|
|
39
|
+
Published at **[nicolaipre.github.io/future-framework](https://nicolaipre.github.io/future-framework/)**. To preview locally: `uv sync --group docs && uv run mkdocs serve`.
|
|
46
40
|
|
|
47
41
|
## Install
|
|
48
42
|
```bash
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
uv add future-framework
|
|
44
|
+
uv run future init myproject
|
|
51
45
|
```
|
|
52
46
|
|
|
53
47
|
Import name is `future` (`from future.application import Future`).
|
|
@@ -71,4 +65,3 @@ app.run(host="127.0.0.1", port=8000)
|
|
|
71
65
|
|
|
72
66
|
## License
|
|
73
67
|
See [LICENSE](LICENSE). Versioning: [CHANGELOG.md](CHANGELOG.md).
|
|
74
|
-
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
[](https://codecov.io/gh/nicolaipre/future-framework)
|
|
7
7
|
|
|
8
8
|
## Documentation
|
|
9
|
-
Published at **[nicolaipre.github.io/future-framework](https://nicolaipre.github.io/future-framework/)**. To preview locally: `
|
|
9
|
+
Published at **[nicolaipre.github.io/future-framework](https://nicolaipre.github.io/future-framework/)**. To preview locally: `uv sync --group docs && uv run mkdocs serve`.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
```bash
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
uv add future-framework
|
|
14
|
+
uv run future init myproject
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Import name is `future` (`from future.application import Future`).
|
|
@@ -23,13 +23,13 @@ STUB_KINDS = {
|
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
cp .env.example .env # DB_DATABASE=database → database.sqlite
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
uv sync
|
|
27
|
+
uv run future make:migration ExampleModel
|
|
28
|
+
uv run future migrate
|
|
29
|
+
uv run future make:seed ExampleModel
|
|
30
|
+
uv run future seed
|
|
31
|
+
uv run python run.py
|
|
32
|
+
uv run future routes
|
|
33
33
|
```
|
|
34
34
|
""").lstrip("\n"),
|
|
35
35
|
},
|
|
@@ -62,23 +62,23 @@ STUB_KINDS = {
|
|
|
62
62
|
"pyproject": {
|
|
63
63
|
"path": "pyproject.toml",
|
|
64
64
|
"template": dedent("""
|
|
65
|
-
[
|
|
65
|
+
[project]
|
|
66
66
|
name = "future-boilerplate"
|
|
67
67
|
version = "0.0.1"
|
|
68
68
|
description = "Boilerplate app for the Future framework."
|
|
69
|
-
|
|
69
|
+
requires-python = ">=3.12,<4.0"
|
|
70
70
|
readme = "README.md"
|
|
71
|
+
dependencies = [
|
|
72
|
+
"python-dotenv>=1.0.1,<2.0.0",
|
|
73
|
+
"future-framework>=3.0.2,<4.0.0",
|
|
74
|
+
]
|
|
71
75
|
|
|
72
|
-
[tool.poetry.dependencies]
|
|
73
|
-
python = "^3.12"
|
|
74
|
-
python-dotenv = "^1.0.1"
|
|
75
|
-
future-framework = "^2.0.0"
|
|
76
76
|
# Or, while developing Future next to this app:
|
|
77
|
-
#
|
|
77
|
+
# [tool.uv.sources]
|
|
78
|
+
# future-framework = { path = "../future-framework", editable = true }
|
|
78
79
|
|
|
79
|
-
[
|
|
80
|
-
|
|
81
|
-
build-backend = "poetry.core.masonry.api"
|
|
80
|
+
[tool.uv]
|
|
81
|
+
package = false
|
|
82
82
|
""").lstrip("\n"),
|
|
83
83
|
},
|
|
84
84
|
"license": {
|
|
@@ -114,7 +114,6 @@ STUB_KINDS = {
|
|
|
114
114
|
.vscode/
|
|
115
115
|
.env
|
|
116
116
|
.venv
|
|
117
|
-
poetry.lock
|
|
118
117
|
__pycache__/
|
|
119
118
|
*.sqlite
|
|
120
119
|
*.sqlite3
|
|
@@ -2,7 +2,7 @@ from datetime import datetime
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
4
|
from future.interfacing import Interface
|
|
5
|
-
from future.
|
|
5
|
+
from future.scheduling import Unit, WorkingHours
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ITask(Interface):
|
|
@@ -11,6 +11,7 @@ class ITask(Interface):
|
|
|
11
11
|
unit: Optional[Unit] = None
|
|
12
12
|
start_time: Optional[datetime] = None
|
|
13
13
|
jitter: Optional[float] = None
|
|
14
|
+
working_hours: Optional[WorkingHours] = None
|
|
14
15
|
|
|
15
16
|
async def run(self) -> None:
|
|
16
17
|
raise NotImplementedError
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from datetime import datetime, time
|
|
2
|
+
from enum import Enum, IntEnum
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Unit(Enum):
|
|
8
|
+
SECONDS = "seconds"
|
|
9
|
+
MINUTES = "minutes"
|
|
10
|
+
HOURS = "hours"
|
|
11
|
+
DAYS = "days"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Weekday(IntEnum):
|
|
15
|
+
MONDAY = 0
|
|
16
|
+
TUESDAY = 1
|
|
17
|
+
WEDNESDAY = 2
|
|
18
|
+
THURSDAY = 3
|
|
19
|
+
FRIDAY = 4
|
|
20
|
+
SATURDAY = 5
|
|
21
|
+
SUNDAY = 6
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WorkingHours:
|
|
25
|
+
"""A recurring local-time window in which a scheduled task may run."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, start: time, end: time, timezone: str = "UTC", weekdays: Optional[frozenset[Weekday]] = None) -> None:
|
|
28
|
+
self.start = start
|
|
29
|
+
self.end = end
|
|
30
|
+
self.timezone = timezone
|
|
31
|
+
self.weekdays = weekdays if weekdays is not None else frozenset({Weekday.MONDAY, Weekday.TUESDAY, Weekday.WEDNESDAY, Weekday.THURSDAY, Weekday.FRIDAY})
|
|
32
|
+
self._validate()
|
|
33
|
+
|
|
34
|
+
def _validate(self) -> None:
|
|
35
|
+
if self.start.tzinfo is not None or self.end.tzinfo is not None:
|
|
36
|
+
raise ValueError("Working-hours start and end must be timezone-naive times")
|
|
37
|
+
if self.start == self.end:
|
|
38
|
+
raise ValueError("Working-hours start and end must be different")
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
ZoneInfo(self.timezone)
|
|
42
|
+
except ZoneInfoNotFoundError as error:
|
|
43
|
+
raise ValueError(f"Unknown working-hours timezone: {self.timezone}") from error
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
weekdays = frozenset(Weekday(day) for day in self.weekdays)
|
|
47
|
+
except ValueError as error:
|
|
48
|
+
raise ValueError("Working-hours weekdays must contain valid Weekday values") from error
|
|
49
|
+
if not weekdays:
|
|
50
|
+
raise ValueError("Working-hours weekdays cannot be empty")
|
|
51
|
+
self.weekdays = weekdays
|
|
52
|
+
|
|
53
|
+
def allows(self, moment: datetime) -> bool:
|
|
54
|
+
"""Return whether an aware instant falls within this working window."""
|
|
55
|
+
if moment.tzinfo is None:
|
|
56
|
+
raise ValueError("WorkingHours.allows requires a timezone-aware datetime")
|
|
57
|
+
|
|
58
|
+
local = moment.astimezone(ZoneInfo(self.timezone))
|
|
59
|
+
local_time = local.time()
|
|
60
|
+
weekday = Weekday(local.weekday())
|
|
61
|
+
|
|
62
|
+
if self.start < self.end:
|
|
63
|
+
return weekday in self.weekdays and self.start <= local_time < self.end
|
|
64
|
+
|
|
65
|
+
# Overnight windows belong to the day on which they open. For example,
|
|
66
|
+
# Friday 22:00-06:00 includes early Saturday morning.
|
|
67
|
+
if local_time >= self.start:
|
|
68
|
+
opening_day = weekday
|
|
69
|
+
elif local_time < self.end:
|
|
70
|
+
opening_day = Weekday((weekday - 1) % 7)
|
|
71
|
+
else:
|
|
72
|
+
return False
|
|
73
|
+
return opening_day in self.weekdays
|
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import random
|
|
3
3
|
|
|
4
|
-
from datetime import datetime, timedelta
|
|
5
|
-
from
|
|
6
|
-
from typing import Optional
|
|
4
|
+
from datetime import datetime, timedelta, timezone as datetime_timezone
|
|
5
|
+
from typing import Callable, Optional
|
|
7
6
|
|
|
8
7
|
from future.logger import log
|
|
8
|
+
from future.scheduling import Unit, Weekday, WorkingHours
|
|
9
9
|
|
|
10
|
+
__all__ = ["CronScheduler", "ScheduledTask", "Unit", "Weekday", "WorkingHours"]
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
|
|
13
|
+
def _as_utc(moment: datetime) -> datetime:
|
|
14
|
+
"""Normalize datetimes for safe comparison and interval arithmetic."""
|
|
15
|
+
if moment.tzinfo is None:
|
|
16
|
+
moment = moment.astimezone()
|
|
17
|
+
return moment.astimezone(datetime_timezone.utc)
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class ScheduledTask:
|
|
19
21
|
"""Represents a scheduled task with its timing configuration."""
|
|
20
22
|
|
|
21
|
-
def __init__(self, task) -> None:
|
|
23
|
+
def __init__(self, task, now: Optional[datetime] = None) -> None:
|
|
22
24
|
self.task = task
|
|
23
25
|
self.name = task.name
|
|
24
26
|
self.interval = task.interval
|
|
25
27
|
self.unit = task.unit
|
|
26
|
-
self.start_time = task.start_time
|
|
28
|
+
self.start_time = _as_utc(task.start_time) if task.start_time is not None else _as_utc(now or datetime.now(datetime_timezone.utc))
|
|
27
29
|
self.jitter = task.jitter
|
|
30
|
+
self.working_hours = getattr(task, "working_hours", None)
|
|
31
|
+
if self.working_hours is not None and not isinstance(self.working_hours, WorkingHours):
|
|
32
|
+
raise TypeError("task.working_hours must be a WorkingHours instance or None")
|
|
28
33
|
self.last_run: Optional[datetime] = None
|
|
29
34
|
self.next_run: Optional[datetime] = None
|
|
30
35
|
self.running = False
|
|
31
36
|
|
|
32
|
-
if self.start_time is None:
|
|
33
|
-
self.start_time = datetime.now()
|
|
34
37
|
self.calculate_next_run()
|
|
35
38
|
|
|
36
39
|
def calculate_next_run(self) -> None:
|
|
@@ -49,21 +52,32 @@ class ScheduledTask:
|
|
|
49
52
|
if self.next_run is not None and self.jitter and self.jitter > 0:
|
|
50
53
|
self.next_run = self.next_run + timedelta(seconds=random.uniform(0, self.jitter))
|
|
51
54
|
|
|
55
|
+
def is_due(self, now: datetime) -> bool:
|
|
56
|
+
"""Return whether the task is due and permitted to run now."""
|
|
57
|
+
now = _as_utc(now)
|
|
58
|
+
if self.running or self.next_run is None or now < self.next_run:
|
|
59
|
+
return False
|
|
60
|
+
return self.working_hours is None or self.working_hours.allows(now)
|
|
61
|
+
|
|
52
62
|
|
|
53
63
|
class CronScheduler:
|
|
54
64
|
"""A cron-like scheduler for running background tasks."""
|
|
55
65
|
|
|
56
|
-
def __init__(self) -> None:
|
|
66
|
+
def __init__(self, clock: Optional[Callable[[], datetime]] = None) -> None:
|
|
57
67
|
self.tasks: dict[str, ScheduledTask] = {}
|
|
58
68
|
self.running = False
|
|
59
69
|
self.check_interval = 1.0 # Check every second for tasks to run
|
|
70
|
+
self._clock = clock or (lambda: datetime.now(datetime_timezone.utc))
|
|
71
|
+
|
|
72
|
+
def _now(self) -> datetime:
|
|
73
|
+
return _as_utc(self._clock())
|
|
60
74
|
|
|
61
75
|
def add_task(self, task) -> None:
|
|
62
76
|
if task.interval is None or task.unit is None or not task.name:
|
|
63
77
|
log.warning(f"Skipping task '{task.name or '?'}' - missing required parameters")
|
|
64
78
|
return
|
|
65
79
|
|
|
66
|
-
scheduled = ScheduledTask(task)
|
|
80
|
+
scheduled = ScheduledTask(task, self._now())
|
|
67
81
|
self.tasks[task.name] = scheduled
|
|
68
82
|
jitter_note = f" (jitter 0–{task.jitter}s)" if task.jitter else ""
|
|
69
83
|
log.info(f"Added scheduled task '{task.name}' to run every {task.interval} {task.unit.value}{jitter_note}")
|
|
@@ -91,7 +105,7 @@ class CronScheduler:
|
|
|
91
105
|
log.debug(f"Running scheduled task '{scheduled.name}'")
|
|
92
106
|
await scheduled.task.run()
|
|
93
107
|
|
|
94
|
-
scheduled.last_run =
|
|
108
|
+
scheduled.last_run = self._now()
|
|
95
109
|
scheduled.calculate_next_run()
|
|
96
110
|
if scheduled.next_run:
|
|
97
111
|
log.debug(f"Completed scheduled task '{scheduled.name}', next run at {scheduled.next_run.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
@@ -109,12 +123,12 @@ class CronScheduler:
|
|
|
109
123
|
log.info("Starting cron scheduler...")
|
|
110
124
|
|
|
111
125
|
while self.running:
|
|
112
|
-
now =
|
|
126
|
+
now = self._now()
|
|
113
127
|
tasks_to_run = []
|
|
114
128
|
|
|
115
129
|
# Check which tasks need to run
|
|
116
130
|
for scheduled in self.tasks.values():
|
|
117
|
-
if scheduled.
|
|
131
|
+
if scheduled.is_due(now):
|
|
118
132
|
scheduled.running = True
|
|
119
133
|
tasks_to_run.append(scheduled)
|
|
120
134
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "future-framework"
|
|
3
|
+
version = "3.1.0"
|
|
4
|
+
description = "ASGI-based framework for web APIs"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
license-files = ["LICENSE"]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.12,<4.0"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"uvicorn>=0.51.0,<0.52.0",
|
|
11
|
+
"httpx>=0.28.1,<0.29.0",
|
|
12
|
+
"rich>=15.0.0,<16.0.0",
|
|
13
|
+
"elasticsearch>=8.15.0,<9.0.0",
|
|
14
|
+
"strawberry-graphql>=0.323.0,<0.324.0",
|
|
15
|
+
"python-dotenv>=1.2.2,<2.0.0",
|
|
16
|
+
"sqlalchemy[asyncio]>=2.0.51,<3.0.0",
|
|
17
|
+
"inflection>=0.5.1,<0.6.0",
|
|
18
|
+
"pymongo>=4.17.0,<5.0.0",
|
|
19
|
+
"aiosqlite>=0.21.0,<0.22.0",
|
|
20
|
+
"aiomysql>=0.2.0,<0.3.0",
|
|
21
|
+
"asyncpg>=0.30.0,<0.31.0",
|
|
22
|
+
"asynch>=0.2.5,<0.3.0",
|
|
23
|
+
"clickhouse-driver>=0.2.11,<0.3.0",
|
|
24
|
+
"redis>=8.0.0,<9.0.0",
|
|
25
|
+
"faker>=40.0.0,<41.0.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[project.authors]]
|
|
29
|
+
name = "nicolaipre"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/nicolaipre/future-framework"
|
|
33
|
+
Repository = "https://github.com/nicolaipre/future-framework"
|
|
34
|
+
Documentation = "https://nicolaipre.github.io/future-framework/"
|
|
35
|
+
"Bug Tracker" = "https://github.com/nicolaipre/future-framework/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
future = "future.cli:main"
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=9.1.0,<10.0.0",
|
|
43
|
+
"pytest-asyncio>=1.4.0,<2.0.0",
|
|
44
|
+
"coverage>=7.15.0,<8.0.0",
|
|
45
|
+
"ruff>=0.15.0,<0.16.0",
|
|
46
|
+
"mypy>=2.3.0,<3.0.0",
|
|
47
|
+
"types-psutil>=7.2.0,<8.0.0",
|
|
48
|
+
"types-requests>=2.32.0,<3.0.0",
|
|
49
|
+
"twine>=6.2.0,<7.0.0",
|
|
50
|
+
]
|
|
51
|
+
docs = ["mkdocs>=1.6.1,<2.0.0"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
asyncio_mode = "auto"
|
|
55
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
line-length = 256
|
|
59
|
+
extend-exclude = [
|
|
60
|
+
".venv",
|
|
61
|
+
"venv",
|
|
62
|
+
"build",
|
|
63
|
+
"dist",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = [
|
|
68
|
+
"E",
|
|
69
|
+
"F",
|
|
70
|
+
"W",
|
|
71
|
+
"B",
|
|
72
|
+
]
|
|
73
|
+
ignore = [
|
|
74
|
+
"E501",
|
|
75
|
+
"E261",
|
|
76
|
+
"E266",
|
|
77
|
+
"E302",
|
|
78
|
+
"E303",
|
|
79
|
+
"E402",
|
|
80
|
+
"E731",
|
|
81
|
+
"W291",
|
|
82
|
+
"W293",
|
|
83
|
+
"B008",
|
|
84
|
+
"B007",
|
|
85
|
+
"B009",
|
|
86
|
+
"B904",
|
|
87
|
+
"F541",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.ruff.lint.per-file-ignores]
|
|
91
|
+
"**/__init__.py" = ["F401"]
|
|
92
|
+
"tests/**" = [
|
|
93
|
+
"B011",
|
|
94
|
+
"F401",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.format]
|
|
98
|
+
quote-style = "double"
|
|
99
|
+
indent-style = "space"
|
|
100
|
+
line-ending = "auto"
|
|
101
|
+
skip-magic-trailing-comma = true
|
|
102
|
+
docstring-code-format = false
|
|
103
|
+
|
|
104
|
+
[tool.uv.build-backend]
|
|
105
|
+
module-name = "future"
|
|
106
|
+
module-root = ""
|
|
107
|
+
|
|
108
|
+
[tool.mypy]
|
|
109
|
+
python_version = "3.12"
|
|
110
|
+
warn_return_any = false
|
|
111
|
+
warn_unused_configs = true
|
|
112
|
+
disallow_untyped_defs = false
|
|
113
|
+
ignore_missing_imports = true
|
|
114
|
+
check_untyped_defs = false
|
|
115
|
+
pretty = true
|
|
116
|
+
disable_error_code = [
|
|
117
|
+
"misc",
|
|
118
|
+
"var-annotated",
|
|
119
|
+
"assignment",
|
|
120
|
+
"arg-type",
|
|
121
|
+
"import-untyped",
|
|
122
|
+
]
|
|
123
|
+
exclude = ['(?x)(build/|dist/|\.venv/)']
|
|
124
|
+
|
|
125
|
+
[build-system]
|
|
126
|
+
requires = ["uv_build>=0.12.18,<0.13"]
|
|
127
|
+
build-backend = "uv_build"
|
|
@@ -1,55 +1,54 @@
|
|
|
1
|
-
[
|
|
1
|
+
[project]
|
|
2
2
|
name = "future-framework"
|
|
3
|
-
version = "3.0
|
|
3
|
+
version = "3.1.0"
|
|
4
4
|
description = "ASGI-based framework for web APIs"
|
|
5
|
-
authors = ["nicolaipre"]
|
|
6
|
-
license = "
|
|
5
|
+
authors = [{ name = "nicolaipre" }]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
7
8
|
readme = "README.md"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
requires-python = ">=3.12,<4.0"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"uvicorn>=0.51.0,<0.52.0",
|
|
12
|
+
"httpx>=0.28.1,<0.29.0",
|
|
13
|
+
"rich>=15.0.0,<16.0.0",
|
|
14
|
+
"elasticsearch>=8.15.0,<9.0.0",
|
|
15
|
+
"strawberry-graphql>=0.323.0,<0.324.0",
|
|
16
|
+
"python-dotenv>=1.2.2,<2.0.0",
|
|
17
|
+
"sqlalchemy[asyncio]>=2.0.51,<3.0.0",
|
|
18
|
+
"inflection>=0.5.1,<0.6.0",
|
|
19
|
+
"pymongo>=4.17.0,<5.0.0",
|
|
20
|
+
"aiosqlite>=0.21.0,<0.22.0",
|
|
21
|
+
"aiomysql>=0.2.0,<0.3.0",
|
|
22
|
+
"asyncpg>=0.30.0,<0.31.0",
|
|
23
|
+
"asynch>=0.2.5,<0.3.0",
|
|
24
|
+
"clickhouse-driver>=0.2.11,<0.3.0",
|
|
25
|
+
"redis>=8.0.0,<9.0.0",
|
|
26
|
+
"faker>=40.0.0,<41.0.0",
|
|
27
|
+
]
|
|
12
28
|
|
|
13
|
-
[
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/nicolaipre/future-framework"
|
|
31
|
+
Repository = "https://github.com/nicolaipre/future-framework"
|
|
32
|
+
Documentation = "https://nicolaipre.github.io/future-framework/"
|
|
14
33
|
"Bug Tracker" = "https://github.com/nicolaipre/future-framework/issues"
|
|
15
34
|
|
|
16
|
-
[
|
|
17
|
-
|
|
18
|
-
uvicorn = "^0.51.0"
|
|
19
|
-
httpx = "^0.28.1"
|
|
20
|
-
rich = "^15.0.0"
|
|
21
|
-
elasticsearch = ">=8.15.0,<9"
|
|
22
|
-
strawberry-graphql = "^0.323.0"
|
|
23
|
-
python-dotenv = "^1.2.2"
|
|
24
|
-
sqlalchemy = "^2.0.51"
|
|
25
|
-
inflection = "^0.5.1"
|
|
26
|
-
pymongo = "^4.17.0"
|
|
27
|
-
aiosqlite = "^0.21.0"
|
|
28
|
-
aiomysql = "^0.2.0"
|
|
29
|
-
asyncpg = "^0.30.0"
|
|
30
|
-
asynch = "^0.2.5"
|
|
31
|
-
clickhouse-driver = "^0.2.11"
|
|
32
|
-
redis = "^8.0.0"
|
|
33
|
-
faker = "^40.0.0"
|
|
34
|
-
|
|
35
|
-
[tool.poetry.group.dev.dependencies]
|
|
36
|
-
pytest = "^9.1.0"
|
|
37
|
-
pytest-asyncio = "^1.4.0"
|
|
38
|
-
coverage = "^7.15.0"
|
|
39
|
-
ruff = "^0.15.0"
|
|
40
|
-
mypy = "^2.3.0"
|
|
41
|
-
types-psutil = "^7.2.0"
|
|
42
|
-
types-requests = "^2.32.0"
|
|
43
|
-
build = "^1.5.0"
|
|
44
|
-
twine = "^6.2.0"
|
|
45
|
-
|
|
46
|
-
[tool.poetry.group.docs.dependencies]
|
|
47
|
-
mkdocs = "^1.6.1"
|
|
35
|
+
[project.scripts]
|
|
36
|
+
future = "future.cli:main"
|
|
48
37
|
|
|
49
|
-
[
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=9.1.0,<10.0.0",
|
|
41
|
+
"pytest-asyncio>=1.4.0,<2.0.0",
|
|
42
|
+
"coverage>=7.15.0,<8.0.0",
|
|
43
|
+
"ruff>=0.15.0,<0.16.0",
|
|
44
|
+
"mypy>=2.3.0,<3.0.0",
|
|
45
|
+
"types-psutil>=7.2.0,<8.0.0",
|
|
46
|
+
"types-requests>=2.32.0,<3.0.0",
|
|
47
|
+
"twine>=6.2.0,<7.0.0",
|
|
48
|
+
]
|
|
49
|
+
docs = [
|
|
50
|
+
"mkdocs>=1.6.1,<2.0.0",
|
|
51
|
+
]
|
|
53
52
|
|
|
54
53
|
[tool.pytest.ini_options]
|
|
55
54
|
asyncio_mode = "auto"
|
|
@@ -98,8 +97,12 @@ skip-magic-trailing-comma = true
|
|
|
98
97
|
docstring-code-format = false
|
|
99
98
|
|
|
100
99
|
[build-system]
|
|
101
|
-
requires = ["
|
|
102
|
-
build-backend = "
|
|
100
|
+
requires = ["uv_build>=0.12.18,<0.13"]
|
|
101
|
+
build-backend = "uv_build"
|
|
102
|
+
|
|
103
|
+
[tool.uv.build-backend]
|
|
104
|
+
module-name = "future"
|
|
105
|
+
module-root = ""
|
|
103
106
|
|
|
104
107
|
[tool.mypy]
|
|
105
108
|
python_version = "3.12"
|
|
@@ -112,6 +115,3 @@ pretty = true
|
|
|
112
115
|
# Gradual typing; Model.__table__ overrides and similar are normal in this codebase.
|
|
113
116
|
disable_error_code = ["misc", "var-annotated", "assignment", "arg-type", "import-untyped"]
|
|
114
117
|
exclude = ["(?x)(build/|dist/|\\.venv/)"]
|
|
115
|
-
|
|
116
|
-
[tool.poetry.scripts]
|
|
117
|
-
future = "future.cli:main"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/Auth0Authentication.py
RENAMED
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/AzureADAuthentication.py
RENAMED
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/BasicAuthentication.py
RENAMED
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/KerberosAuthentication.py
RENAMED
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/KeycloakAuthentication.py
RENAMED
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/OAuth2Authentication.py
RENAMED
|
File without changes
|
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/authentication/SAMLAuthentication.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/HTAccessConfuserMiddleware.py
RENAMED
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/HeaderConfuserMiddleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/SQLiConfuserMiddleware.py
RENAMED
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/ScopeValidationMiddleware.py
RENAMED
|
File without changes
|
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/TestMiddlewareRequest.py
RENAMED
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/TestMiddlewareResponse.py
RENAMED
|
File without changes
|
{future_framework-3.0.2 → future_framework-3.1.0}/future/middleware/WebServerConfuserMiddleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|