fakts-next 1.0.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.
- fakts_next-1.0.0/PKG-INFO +111 -0
- fakts_next-1.0.0/README.md +83 -0
- fakts_next-1.0.0/fakts_next/__init__.py +32 -0
- fakts_next-1.0.0/fakts_next/cache/file.py +148 -0
- fakts_next-1.0.0/fakts_next/cache/model.py +11 -0
- fakts_next-1.0.0/fakts_next/cache/nocache.py +55 -0
- fakts_next-1.0.0/fakts_next/cache/qt/settings.py +79 -0
- fakts_next-1.0.0/fakts_next/cli/__init__.py +12 -0
- fakts_next-1.0.0/fakts_next/cli/advertise.py +134 -0
- fakts_next-1.0.0/fakts_next/cli/main.py +156 -0
- fakts_next-1.0.0/fakts_next/contrib/__init__.py +0 -0
- fakts_next-1.0.0/fakts_next/contrib/rath/__init__.py +0 -0
- fakts_next-1.0.0/fakts_next/contrib/rath/aiohttp.py +53 -0
- fakts_next-1.0.0/fakts_next/contrib/rath/graphql_ws.py +47 -0
- fakts_next-1.0.0/fakts_next/contrib/rath/httpx.py +47 -0
- fakts_next-1.0.0/fakts_next/contrib/rath/subscription_transport_ws.py +47 -0
- fakts_next-1.0.0/fakts_next/errors.py +43 -0
- fakts_next-1.0.0/fakts_next/fakts.py +345 -0
- fakts_next-1.0.0/fakts_next/grants/__init__.py +47 -0
- fakts_next-1.0.0/fakts_next/grants/base.py +54 -0
- fakts_next-1.0.0/fakts_next/grants/env.py +110 -0
- fakts_next-1.0.0/fakts_next/grants/errors.py +5 -0
- fakts_next-1.0.0/fakts_next/grants/io/__init__.py +5 -0
- fakts_next-1.0.0/fakts_next/grants/io/qt/yaml.py +164 -0
- fakts_next-1.0.0/fakts_next/grants/io/toml.py +52 -0
- fakts_next-1.0.0/fakts_next/grants/io/yaml.py +26 -0
- fakts_next-1.0.0/fakts_next/grants/meta/__init__.py +10 -0
- fakts_next-1.0.0/fakts_next/grants/meta/failsafe.py +78 -0
- fakts_next-1.0.0/fakts_next/grants/meta/parallel.py +62 -0
- fakts_next-1.0.0/fakts_next/grants/remote/__init__.py +15 -0
- fakts_next-1.0.0/fakts_next/grants/remote/base.py +78 -0
- fakts_next-1.0.0/fakts_next/grants/remote/builders.py +69 -0
- fakts_next-1.0.0/fakts_next/grants/remote/claimers/__init__.py +15 -0
- fakts_next-1.0.0/fakts_next/grants/remote/claimers/post.py +78 -0
- fakts_next-1.0.0/fakts_next/grants/remote/claimers/static.py +26 -0
- fakts_next-1.0.0/fakts_next/grants/remote/demanders/__init__.py +11 -0
- fakts_next-1.0.0/fakts_next/grants/remote/demanders/device_code.py +227 -0
- fakts_next-1.0.0/fakts_next/grants/remote/demanders/qt/__init__.py +7 -0
- fakts_next-1.0.0/fakts_next/grants/remote/demanders/qt/auto_save_token_widget.py +58 -0
- fakts_next-1.0.0/fakts_next/grants/remote/demanders/qt/qt_settings_token_store.py +92 -0
- fakts_next-1.0.0/fakts_next/grants/remote/demanders/redeem.py +123 -0
- fakts_next-1.0.0/fakts_next/grants/remote/demanders/retrieve.py +118 -0
- fakts_next-1.0.0/fakts_next/grants/remote/demanders/static.py +45 -0
- fakts_next-1.0.0/fakts_next/grants/remote/demanders/utils.py +118 -0
- fakts_next-1.0.0/fakts_next/grants/remote/discovery/__init__.py +15 -0
- fakts_next-1.0.0/fakts_next/grants/remote/discovery/advertised.py +244 -0
- fakts_next-1.0.0/fakts_next/grants/remote/discovery/qt/__init__.py +7 -0
- fakts_next-1.0.0/fakts_next/grants/remote/discovery/qt/selectable_beacon.py +457 -0
- fakts_next-1.0.0/fakts_next/grants/remote/discovery/static.py +30 -0
- fakts_next-1.0.0/fakts_next/grants/remote/discovery/utils.py +131 -0
- fakts_next-1.0.0/fakts_next/grants/remote/discovery/well_known.py +75 -0
- fakts_next-1.0.0/fakts_next/grants/remote/errors.py +25 -0
- fakts_next-1.0.0/fakts_next/grants/remote/models.py +114 -0
- fakts_next-1.0.0/fakts_next/protocols.py +130 -0
- fakts_next-1.0.0/fakts_next/utils.py +26 -0
- fakts_next-1.0.0/pyproject.toml +106 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fakts-next
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: asynchronous configuration provider ( tailored to support dynamic client-server relations)
|
|
5
|
+
License: CC BY-NC 3.0
|
|
6
|
+
Author: jhnnsrs
|
|
7
|
+
Author-email: jhnnsrs@gmail.com
|
|
8
|
+
Requires-Python: >=3.8,<4.0
|
|
9
|
+
Classifier: License :: Other/Proprietary License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Provides-Extra: cli
|
|
17
|
+
Provides-Extra: remote
|
|
18
|
+
Requires-Dist: PyYAML (>=6)
|
|
19
|
+
Requires-Dist: QtPy (>=2.0.1,<3.0.0)
|
|
20
|
+
Requires-Dist: aiohttp (>=3.8.2,<4.0.0) ; extra == "remote"
|
|
21
|
+
Requires-Dist: certifi (>2021) ; extra == "remote"
|
|
22
|
+
Requires-Dist: koil (>=1.0.0)
|
|
23
|
+
Requires-Dist: netifaces (>=0.11.0,<0.12.0) ; extra == "cli"
|
|
24
|
+
Requires-Dist: pydantic (>2)
|
|
25
|
+
Requires-Dist: rich_click (>=0.3) ; extra == "cli"
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# fakts
|
|
29
|
+
|
|
30
|
+
[](https://codecov.io/gh/jhnnsrs/fakts)
|
|
31
|
+
[](https://pypi.org/project/fakts/)
|
|
32
|
+
[](https://pypi.org/project/fakts/)
|
|
33
|
+

|
|
34
|
+
[](https://pypi.python.org/pypi/fakts/)
|
|
35
|
+
[](https://pypi.python.org/pypi/fakts/)
|
|
36
|
+
[](https://pypi.python.org/pypi/fakts/)
|
|
37
|
+
[](https://github.com/psf/black)
|
|
38
|
+
[](http://mypy-lang.org/)
|
|
39
|
+
[](https://github.com/jhnnsrs/fakts)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Inspiration
|
|
43
|
+
|
|
44
|
+
Fakts was designed to make configuration of apps compatible with modern concurrency patterns, it is designed to allow
|
|
45
|
+
for asynchronous retrieval of configuration from various sources, may it be a config file, environmental variables
|
|
46
|
+
or a remote server (via the "fakts remote protocol", described in the documentation.).
|
|
47
|
+
|
|
48
|
+
Fakts was conceived as a way to provide a configuration interface for the [arkitekt](https://arkitekt.live) platform,
|
|
49
|
+
where clients needed to dynamically retrieve configuration from a remote server, but it is designed to be used in any python project.
|
|
50
|
+
|
|
51
|
+
# Core Design
|
|
52
|
+
|
|
53
|
+
Fakts uses `Grants` to obtain configuration asynchronously, a grant is a way of retrieving the configuration from a
|
|
54
|
+
specific source. It can be a local config file (eg. yaml, toml, json), environemnt variables, a remote configuration (eg. from a fakts server), or a database.
|
|
55
|
+
The `Fakts` class then wraps the grant to ensure both a sychronous and asychronous interface that is threadsafe.
|
|
56
|
+
|
|
57
|
+
Grants are designed to be composable through `MetaGrants` so by desigining a specifc grant structure, one can
|
|
58
|
+
highly customize the retrieval logic. Please check out the documentation for more information.
|
|
59
|
+
|
|
60
|
+
# Example:
|
|
61
|
+
|
|
62
|
+
By default fakts follows a key-value structure, where the key is a string, and the value can be any serializable
|
|
63
|
+
python object.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
async with Fakts(grant=YamlGrant("config.yaml")) as fakts:
|
|
67
|
+
config = await fakts.aget("group_name")
|
|
68
|
+
# will return the configuration for the group_name key in the yaml file
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
or
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
with Fakts(grant=YamlGrant("config.yaml")) as fakts:
|
|
75
|
+
value = fakts.get("nested.key.path")
|
|
76
|
+
# will return the configuration for a nested key in the yaml file
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Fakts should be used as a context manager, and will set the current fakts context variable to itself, letting
|
|
80
|
+
you access the current fakts instance from anywhere in your code (async or sync) without specifically passing a referece.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Composability
|
|
84
|
+
|
|
85
|
+
You can compose grants through meta grants in order to load configuration from multiple sources (eg. a local, file
|
|
86
|
+
that can be overwritten by a remote configuration, or some envionment variables).
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
async with Fakts(grant=FailsafeGrant(
|
|
92
|
+
grants=[
|
|
93
|
+
EnvGrant(),
|
|
94
|
+
YamlGrant("config.yaml")
|
|
95
|
+
]
|
|
96
|
+
)) as fakts:
|
|
97
|
+
config = await fakts.get("group_name")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
In this example fakts will load the configuration from the environment variables first, and if that fails,
|
|
101
|
+
it will load it from the yaml file.
|
|
102
|
+
|
|
103
|
+
## Fakts Remote Protocol
|
|
104
|
+
|
|
105
|
+
Fakts provides the remote grant protocol for retrieval of configuration in dynamic client-server relationships.
|
|
106
|
+
With these grants you provide a software manifest for a configuration server (fakts-server), that then grants
|
|
107
|
+
the configuration (either through user approval (similar to device code grant)). These grants are mainly used
|
|
108
|
+
to setup or claim an oauth2 application on the backend securely that then can be used to identify the application in the
|
|
109
|
+
future. These grants are at the moment highly specific to the arkitekt platform and subject to change.
|
|
110
|
+
|
|
111
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# fakts
|
|
2
|
+
|
|
3
|
+
[](https://codecov.io/gh/jhnnsrs/fakts)
|
|
4
|
+
[](https://pypi.org/project/fakts/)
|
|
5
|
+
[](https://pypi.org/project/fakts/)
|
|
6
|
+

|
|
7
|
+
[](https://pypi.python.org/pypi/fakts/)
|
|
8
|
+
[](https://pypi.python.org/pypi/fakts/)
|
|
9
|
+
[](https://pypi.python.org/pypi/fakts/)
|
|
10
|
+
[](https://github.com/psf/black)
|
|
11
|
+
[](http://mypy-lang.org/)
|
|
12
|
+
[](https://github.com/jhnnsrs/fakts)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Inspiration
|
|
16
|
+
|
|
17
|
+
Fakts was designed to make configuration of apps compatible with modern concurrency patterns, it is designed to allow
|
|
18
|
+
for asynchronous retrieval of configuration from various sources, may it be a config file, environmental variables
|
|
19
|
+
or a remote server (via the "fakts remote protocol", described in the documentation.).
|
|
20
|
+
|
|
21
|
+
Fakts was conceived as a way to provide a configuration interface for the [arkitekt](https://arkitekt.live) platform,
|
|
22
|
+
where clients needed to dynamically retrieve configuration from a remote server, but it is designed to be used in any python project.
|
|
23
|
+
|
|
24
|
+
# Core Design
|
|
25
|
+
|
|
26
|
+
Fakts uses `Grants` to obtain configuration asynchronously, a grant is a way of retrieving the configuration from a
|
|
27
|
+
specific source. It can be a local config file (eg. yaml, toml, json), environemnt variables, a remote configuration (eg. from a fakts server), or a database.
|
|
28
|
+
The `Fakts` class then wraps the grant to ensure both a sychronous and asychronous interface that is threadsafe.
|
|
29
|
+
|
|
30
|
+
Grants are designed to be composable through `MetaGrants` so by desigining a specifc grant structure, one can
|
|
31
|
+
highly customize the retrieval logic. Please check out the documentation for more information.
|
|
32
|
+
|
|
33
|
+
# Example:
|
|
34
|
+
|
|
35
|
+
By default fakts follows a key-value structure, where the key is a string, and the value can be any serializable
|
|
36
|
+
python object.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
async with Fakts(grant=YamlGrant("config.yaml")) as fakts:
|
|
40
|
+
config = await fakts.aget("group_name")
|
|
41
|
+
# will return the configuration for the group_name key in the yaml file
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
or
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
with Fakts(grant=YamlGrant("config.yaml")) as fakts:
|
|
48
|
+
value = fakts.get("nested.key.path")
|
|
49
|
+
# will return the configuration for a nested key in the yaml file
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Fakts should be used as a context manager, and will set the current fakts context variable to itself, letting
|
|
53
|
+
you access the current fakts instance from anywhere in your code (async or sync) without specifically passing a referece.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Composability
|
|
57
|
+
|
|
58
|
+
You can compose grants through meta grants in order to load configuration from multiple sources (eg. a local, file
|
|
59
|
+
that can be overwritten by a remote configuration, or some envionment variables).
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
async with Fakts(grant=FailsafeGrant(
|
|
65
|
+
grants=[
|
|
66
|
+
EnvGrant(),
|
|
67
|
+
YamlGrant("config.yaml")
|
|
68
|
+
]
|
|
69
|
+
)) as fakts:
|
|
70
|
+
config = await fakts.get("group_name")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
In this example fakts will load the configuration from the environment variables first, and if that fails,
|
|
74
|
+
it will load it from the yaml file.
|
|
75
|
+
|
|
76
|
+
## Fakts Remote Protocol
|
|
77
|
+
|
|
78
|
+
Fakts provides the remote grant protocol for retrieval of configuration in dynamic client-server relationships.
|
|
79
|
+
With these grants you provide a software manifest for a configuration server (fakts-server), that then grants
|
|
80
|
+
the configuration (either through user approval (similar to device code grant)). These grants are mainly used
|
|
81
|
+
to setup or claim an oauth2 application on the backend securely that then can be used to identify the application in the
|
|
82
|
+
future. These grants are at the moment highly specific to the arkitekt platform and subject to change.
|
|
83
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Fakts package.
|
|
2
|
+
|
|
3
|
+
Fakts is a configuration management library. It allows you to
|
|
4
|
+
load configuration from different sources and use it in your
|
|
5
|
+
application.
|
|
6
|
+
|
|
7
|
+
In comparison to other configuration management libraries, Fakts
|
|
8
|
+
is designed to be used in an async environment, or with primarily
|
|
9
|
+
async grants (e.g. a database). It also allows you to use multiple
|
|
10
|
+
grants at the same time, and will merge the results together.
|
|
11
|
+
|
|
12
|
+
Fakts comes also with a few remote grants, that allow you to connect
|
|
13
|
+
to a remote configuration server, and fetch the configuration from
|
|
14
|
+
there, and follows a similar design the oauth2 protocol, to allow for
|
|
15
|
+
safe and secure configuration management.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .fakts import Fakts, FaktsGrant, get_current_fakts_next
|
|
20
|
+
from .errors import FaktsError
|
|
21
|
+
from .grants import EnvGrant, GrantError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Fakts",
|
|
26
|
+
"Fakt",
|
|
27
|
+
"FaktsGrant",
|
|
28
|
+
"EnvGrant",
|
|
29
|
+
"GrantError",
|
|
30
|
+
"get_current_fakts_next",
|
|
31
|
+
"FaktsError",
|
|
32
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
import pydantic
|
|
4
|
+
import datetime
|
|
5
|
+
import logging
|
|
6
|
+
import json
|
|
7
|
+
from fakts_next.protocols import FaktValue, FaktsGrant
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CacheFile(pydantic.BaseModel):
|
|
13
|
+
"""Cache file model"""
|
|
14
|
+
|
|
15
|
+
config: Dict[str, Any]
|
|
16
|
+
created: datetime.datetime
|
|
17
|
+
hash: str = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FileCache(pydantic.BaseModel):
|
|
21
|
+
"""Grant that caches the result of another grant
|
|
22
|
+
|
|
23
|
+
This grant will cache the result of another grant in a file.
|
|
24
|
+
It will load the grant on the first call, and then will load
|
|
25
|
+
the cached version of the grant.
|
|
26
|
+
|
|
27
|
+
Only if the cache is expired, or a "hash" value that is passed
|
|
28
|
+
to the grant is different from the one in the cache, will it
|
|
29
|
+
load the grant again.
|
|
30
|
+
|
|
31
|
+
You can set the expires_in parameter to set the time in seconds
|
|
32
|
+
for the cache to expire.
|
|
33
|
+
|
|
34
|
+
FaktsRequest context parameters:
|
|
35
|
+
- allow_cache: bool - whether to allow the grant to use the cache
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Attributes
|
|
39
|
+
----------
|
|
40
|
+
grant : FaktsGrant
|
|
41
|
+
The grant to cache
|
|
42
|
+
cache_file : str
|
|
43
|
+
The path to the cache file
|
|
44
|
+
hash : str
|
|
45
|
+
The hash to validate the cache against
|
|
46
|
+
expires_in : Optional[int]
|
|
47
|
+
The time in seconds for the cache to expire
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
|
|
53
|
+
"""The grant to cache"""
|
|
54
|
+
|
|
55
|
+
cache_file: str = ".fakts_cache.json"
|
|
56
|
+
"""The path to the cache file"""
|
|
57
|
+
hash: str = pydantic.Field(
|
|
58
|
+
default_factory=lambda: "",
|
|
59
|
+
description="Validating against the hash of the config",
|
|
60
|
+
)
|
|
61
|
+
"""The hash to validate the cache against (if this value differes from the one in the cache, the grant will be reloaded)"""
|
|
62
|
+
|
|
63
|
+
expires_in: Optional[int] = None
|
|
64
|
+
"""When should the cache expire"""
|
|
65
|
+
|
|
66
|
+
async def aload(self) -> Optional[Dict[str, FaktValue]]:
|
|
67
|
+
"""Loads the configuration from the grant
|
|
68
|
+
|
|
69
|
+
It will try to load the configuration from the cache file.
|
|
70
|
+
If the cache is expired, or the hash value is different from
|
|
71
|
+
the one in the cache, it will load the grant again.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
request : FaktsRequest
|
|
76
|
+
The request object that may contain additional information needed for loading the configuration.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
dict
|
|
81
|
+
The configuration loaded from the grant.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
cache = None
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
os.path.exists(self.cache_file)
|
|
90
|
+
):
|
|
91
|
+
with open(self.cache_file, "r") as f:
|
|
92
|
+
x = json.load(f)
|
|
93
|
+
try:
|
|
94
|
+
cache = CacheFile(**x)
|
|
95
|
+
|
|
96
|
+
if self.hash and cache.hash != self.hash:
|
|
97
|
+
cache = None
|
|
98
|
+
|
|
99
|
+
elif self.expires_in:
|
|
100
|
+
if (
|
|
101
|
+
cache.created + datetime.timedelta(seconds=self.expires_in)
|
|
102
|
+
< datetime.datetime.now()
|
|
103
|
+
):
|
|
104
|
+
cache = None
|
|
105
|
+
|
|
106
|
+
except pydantic.ValidationError as e:
|
|
107
|
+
logger.error(f"Could not load cache file: {e}. Ignoring it")
|
|
108
|
+
|
|
109
|
+
if cache is None:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
return cache.config
|
|
114
|
+
|
|
115
|
+
async def aset(self, value: Dict[str, FaktValue]):
|
|
116
|
+
"""Refreshes the configuration from the grant
|
|
117
|
+
|
|
118
|
+
This function is used to refresh the configuration from the grant.
|
|
119
|
+
This is used to refresh the configuration from the grant, and should
|
|
120
|
+
be used to refresh the configuration from the grant.
|
|
121
|
+
|
|
122
|
+
The request object is used to pass information
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
cache = CacheFile(
|
|
126
|
+
config=value, created=datetime.datetime.now(), hash=self.hash
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
with open(self.cache_file, "w+") as f:
|
|
130
|
+
print("Setting cache", cache.model_dump_json())
|
|
131
|
+
json.dump(json.loads(cache.model_dump_json()), f)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def areset(self):
|
|
135
|
+
"""Resets the cache
|
|
136
|
+
|
|
137
|
+
This function is used to reset the cache.
|
|
138
|
+
This is used to reset the cache, and should
|
|
139
|
+
be used to reset the cache.
|
|
140
|
+
|
|
141
|
+
The request object is used to pass information
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
if os.path.exists(self.cache_file):
|
|
145
|
+
os.remove(self.cache_file)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
from fakts_next.protocols import FaktValue, FaktsGrant
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NoCache:
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def aload(self) -> Optional[Dict[str, FaktValue]]:
|
|
10
|
+
"""Loads the configuration from the grant
|
|
11
|
+
|
|
12
|
+
It will try to load the configuration from the cache file.
|
|
13
|
+
If the cache is expired, or the hash value is different from
|
|
14
|
+
the one in the cache, it will load the grant again.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
request : FaktsRequest
|
|
19
|
+
The request object that may contain additional information needed for loading the configuration.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
dict
|
|
24
|
+
The configuration loaded from the grant.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
cache = None
|
|
30
|
+
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
async def aset(self, value: Dict[str, FaktValue]):
|
|
34
|
+
"""Refreshes the configuration from the grant
|
|
35
|
+
|
|
36
|
+
This function is used to refresh the configuration from the grant.
|
|
37
|
+
This is used to refresh the configuration from the grant, and should
|
|
38
|
+
be used to refresh the configuration from the grant.
|
|
39
|
+
|
|
40
|
+
The request object is used to pass information
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
async def areset(self):
|
|
46
|
+
"""Resets the cache
|
|
47
|
+
|
|
48
|
+
This function is used to reset the cache.
|
|
49
|
+
This is used to reset the cache, and should
|
|
50
|
+
be used to reset the cache.
|
|
51
|
+
|
|
52
|
+
The request object is used to pass information
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
pass
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from qtpy import QtCore
|
|
3
|
+
|
|
4
|
+
from fakts.grants.remote.models import FaktsEndpoint
|
|
5
|
+
from typing import Optional, Dict
|
|
6
|
+
import datetime
|
|
7
|
+
from fakts.protocols import FaktValue
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
from fakts_next.cache.model import CacheModel
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class QtSettingsCache(BaseModel):
|
|
14
|
+
"""Retrieves and stores users matching the currently
|
|
15
|
+
active fakts grant"""
|
|
16
|
+
|
|
17
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
18
|
+
settings: QtCore.QSettings
|
|
19
|
+
save_key: str
|
|
20
|
+
hash: str = Field(
|
|
21
|
+
default_factory=lambda: "",
|
|
22
|
+
description="Validating against the hash of the config",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def aset(self, value: Dict[str, FaktValue]) -> None:
|
|
27
|
+
"""Stores the value in the settings
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
value : Dict[str, FaktValue]
|
|
32
|
+
The value to store
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
cache = CacheModel(
|
|
36
|
+
config=value, created=datetime.datetime.now(), hash=self.hash
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
self.settings.setValue(self.save_key, cache.model_dump_json())
|
|
43
|
+
|
|
44
|
+
async def aload(self) -> Optional[Dict[str, FaktValue]]:
|
|
45
|
+
"""Loads the value from the settings
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
Optional[Dict[str, FaktValue]]
|
|
50
|
+
The value, or None if there is no value
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
un_storage = self.settings.value(self.save_key, None)
|
|
54
|
+
if not un_storage:
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
storage = CacheModel.model_validate_json(un_storage)
|
|
58
|
+
if storage.hash != self.hash:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
return storage.config
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(e)
|
|
66
|
+
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
async def areset(self) -> Optional[FaktsEndpoint]:
|
|
70
|
+
"""A function that gets the default endpoint
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
Optional[FaktsEndpoint]
|
|
75
|
+
The stored endpoint, or None if there is no endpoint
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
self.settings.setValue(self.save_key, None)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
""" Fakts CLI module.
|
|
2
|
+
|
|
3
|
+
This module contains the CLI for the fakts_next package.
|
|
4
|
+
It allows you act and interface with the fakts_next package
|
|
5
|
+
from the command line.
|
|
6
|
+
|
|
7
|
+
Currently it only supports advertising a fakts_next endpoint
|
|
8
|
+
on the local network, but in the future it will also
|
|
9
|
+
support other features, like fetching the configuration
|
|
10
|
+
from a remote endpoint.
|
|
11
|
+
|
|
12
|
+
"""
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from socket import AF_INET, SOCK_DGRAM, SOL_SOCKET, SO_BROADCAST, socket
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BeaconProtocol(asyncio.DatagramProtocol):
|
|
13
|
+
"""A protocol that sends beacons to a broadcast address
|
|
14
|
+
|
|
15
|
+
This protocol is used to send beacons to a broadcast address.
|
|
16
|
+
It is used by the advertise function.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AdvertiseBeacon(BaseModel):
|
|
25
|
+
"""A beacon that is sent when advertising"""
|
|
26
|
+
|
|
27
|
+
url: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AdvertiseBinding(BaseModel):
|
|
31
|
+
"""A binding for the advertise function
|
|
32
|
+
|
|
33
|
+
This binding specifies the interface to use, the broadcast address
|
|
34
|
+
and the address to bind to. It also specifies the port to use and
|
|
35
|
+
a magic phrase that is used to identify the beacon.
|
|
36
|
+
|
|
37
|
+
It is retrieved by the retrieve_bindings function, and
|
|
38
|
+
used by the advertise function.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
interface: str
|
|
43
|
+
broadcast_addr: str
|
|
44
|
+
bind_addr: str
|
|
45
|
+
broadcast_port: int = 45678
|
|
46
|
+
magic_phrase: str = "beacon-fakts_next"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def retrieve_bindings() -> List[AdvertiseBinding]:
|
|
50
|
+
"""Uses the netifaces library to retrieve all available interfaces and
|
|
51
|
+
if they are up and running and have a broadcast address, it will return
|
|
52
|
+
a list of bindings for the beacon to use.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ImportError: An importError is raised if the netifaces library is not installed
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List[Binding]: The list of bindings
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
import netifaces
|
|
63
|
+
except ImportError as e:
|
|
64
|
+
raise ImportError(
|
|
65
|
+
"netifaces is required to use the advertised discovery. please install it seperately or install fakts_next with the 'beacon' extras"
|
|
66
|
+
) from e
|
|
67
|
+
|
|
68
|
+
potential_bindings: List[AdvertiseBinding] = []
|
|
69
|
+
|
|
70
|
+
for interface in netifaces.interfaces():
|
|
71
|
+
addrs = netifaces.ifaddresses(interface)
|
|
72
|
+
if netifaces.AF_INET in addrs:
|
|
73
|
+
informations = addrs[netifaces.AF_INET]
|
|
74
|
+
for i in informations:
|
|
75
|
+
if "broadcast" in i:
|
|
76
|
+
potential_bindings.append(
|
|
77
|
+
AdvertiseBinding(
|
|
78
|
+
interface=interface,
|
|
79
|
+
bind_addr=i["addr"],
|
|
80
|
+
broadcast_addr=i["broadcast"],
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return potential_bindings
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def advertise(
|
|
87
|
+
binding: AdvertiseBinding,
|
|
88
|
+
endpoints: List[AdvertiseBeacon],
|
|
89
|
+
interval: int = 1,
|
|
90
|
+
iterations: int = 10,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Advertises the given endpoints on the given binding
|
|
93
|
+
|
|
94
|
+
This function opens a udp socket and sends the endpoints as json to the broadcast address
|
|
95
|
+
on the given port. It will repeat this for the given number of iterations with the given
|
|
96
|
+
interval in between.
|
|
97
|
+
|
|
98
|
+
If interval is -1 it will repeat forever, until this task is cancelled
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
binding (Binding): The binding to use (interface, broadcast address, port)
|
|
102
|
+
endpoints (List[FaktsEndpoint]): The list of endpoints to advertise
|
|
103
|
+
interval (int, optional): The interval between a beacon send in seconds. Defaults to 1.
|
|
104
|
+
iterations (int, optional): The amount of sends that should happen, -1 means infinite (until cancelled). Defaults to 10.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
s = socket(AF_INET, SOCK_DGRAM) # create UDP socket.
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
s.bind((binding.bind_addr, 0))
|
|
112
|
+
s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) # this is a broadcast socket
|
|
113
|
+
|
|
114
|
+
loop = asyncio.get_event_loop()
|
|
115
|
+
transport, pr = await loop.create_datagram_endpoint(BeaconProtocol, sock=s)
|
|
116
|
+
|
|
117
|
+
messages = [
|
|
118
|
+
bytes(binding.magic_phrase + json.dumps(beacon.model_dump()), "utf8")
|
|
119
|
+
for beacon in endpoints
|
|
120
|
+
]
|
|
121
|
+
i = 1
|
|
122
|
+
while i <= iterations or iterations == -1:
|
|
123
|
+
for message in messages:
|
|
124
|
+
transport.sendto( # type: ignore
|
|
125
|
+
message, (binding.broadcast_addr, binding.broadcast_port)
|
|
126
|
+
)
|
|
127
|
+
logger.debug(f"Send Message {message!r}")
|
|
128
|
+
|
|
129
|
+
await asyncio.sleep(interval)
|
|
130
|
+
i += 1
|
|
131
|
+
except asyncio.CancelledError as e:
|
|
132
|
+
transport.close()
|
|
133
|
+
s.close()
|
|
134
|
+
raise e
|