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.
Files changed (56) hide show
  1. fakts_next-1.0.0/PKG-INFO +111 -0
  2. fakts_next-1.0.0/README.md +83 -0
  3. fakts_next-1.0.0/fakts_next/__init__.py +32 -0
  4. fakts_next-1.0.0/fakts_next/cache/file.py +148 -0
  5. fakts_next-1.0.0/fakts_next/cache/model.py +11 -0
  6. fakts_next-1.0.0/fakts_next/cache/nocache.py +55 -0
  7. fakts_next-1.0.0/fakts_next/cache/qt/settings.py +79 -0
  8. fakts_next-1.0.0/fakts_next/cli/__init__.py +12 -0
  9. fakts_next-1.0.0/fakts_next/cli/advertise.py +134 -0
  10. fakts_next-1.0.0/fakts_next/cli/main.py +156 -0
  11. fakts_next-1.0.0/fakts_next/contrib/__init__.py +0 -0
  12. fakts_next-1.0.0/fakts_next/contrib/rath/__init__.py +0 -0
  13. fakts_next-1.0.0/fakts_next/contrib/rath/aiohttp.py +53 -0
  14. fakts_next-1.0.0/fakts_next/contrib/rath/graphql_ws.py +47 -0
  15. fakts_next-1.0.0/fakts_next/contrib/rath/httpx.py +47 -0
  16. fakts_next-1.0.0/fakts_next/contrib/rath/subscription_transport_ws.py +47 -0
  17. fakts_next-1.0.0/fakts_next/errors.py +43 -0
  18. fakts_next-1.0.0/fakts_next/fakts.py +345 -0
  19. fakts_next-1.0.0/fakts_next/grants/__init__.py +47 -0
  20. fakts_next-1.0.0/fakts_next/grants/base.py +54 -0
  21. fakts_next-1.0.0/fakts_next/grants/env.py +110 -0
  22. fakts_next-1.0.0/fakts_next/grants/errors.py +5 -0
  23. fakts_next-1.0.0/fakts_next/grants/io/__init__.py +5 -0
  24. fakts_next-1.0.0/fakts_next/grants/io/qt/yaml.py +164 -0
  25. fakts_next-1.0.0/fakts_next/grants/io/toml.py +52 -0
  26. fakts_next-1.0.0/fakts_next/grants/io/yaml.py +26 -0
  27. fakts_next-1.0.0/fakts_next/grants/meta/__init__.py +10 -0
  28. fakts_next-1.0.0/fakts_next/grants/meta/failsafe.py +78 -0
  29. fakts_next-1.0.0/fakts_next/grants/meta/parallel.py +62 -0
  30. fakts_next-1.0.0/fakts_next/grants/remote/__init__.py +15 -0
  31. fakts_next-1.0.0/fakts_next/grants/remote/base.py +78 -0
  32. fakts_next-1.0.0/fakts_next/grants/remote/builders.py +69 -0
  33. fakts_next-1.0.0/fakts_next/grants/remote/claimers/__init__.py +15 -0
  34. fakts_next-1.0.0/fakts_next/grants/remote/claimers/post.py +78 -0
  35. fakts_next-1.0.0/fakts_next/grants/remote/claimers/static.py +26 -0
  36. fakts_next-1.0.0/fakts_next/grants/remote/demanders/__init__.py +11 -0
  37. fakts_next-1.0.0/fakts_next/grants/remote/demanders/device_code.py +227 -0
  38. fakts_next-1.0.0/fakts_next/grants/remote/demanders/qt/__init__.py +7 -0
  39. fakts_next-1.0.0/fakts_next/grants/remote/demanders/qt/auto_save_token_widget.py +58 -0
  40. fakts_next-1.0.0/fakts_next/grants/remote/demanders/qt/qt_settings_token_store.py +92 -0
  41. fakts_next-1.0.0/fakts_next/grants/remote/demanders/redeem.py +123 -0
  42. fakts_next-1.0.0/fakts_next/grants/remote/demanders/retrieve.py +118 -0
  43. fakts_next-1.0.0/fakts_next/grants/remote/demanders/static.py +45 -0
  44. fakts_next-1.0.0/fakts_next/grants/remote/demanders/utils.py +118 -0
  45. fakts_next-1.0.0/fakts_next/grants/remote/discovery/__init__.py +15 -0
  46. fakts_next-1.0.0/fakts_next/grants/remote/discovery/advertised.py +244 -0
  47. fakts_next-1.0.0/fakts_next/grants/remote/discovery/qt/__init__.py +7 -0
  48. fakts_next-1.0.0/fakts_next/grants/remote/discovery/qt/selectable_beacon.py +457 -0
  49. fakts_next-1.0.0/fakts_next/grants/remote/discovery/static.py +30 -0
  50. fakts_next-1.0.0/fakts_next/grants/remote/discovery/utils.py +131 -0
  51. fakts_next-1.0.0/fakts_next/grants/remote/discovery/well_known.py +75 -0
  52. fakts_next-1.0.0/fakts_next/grants/remote/errors.py +25 -0
  53. fakts_next-1.0.0/fakts_next/grants/remote/models.py +114 -0
  54. fakts_next-1.0.0/fakts_next/protocols.py +130 -0
  55. fakts_next-1.0.0/fakts_next/utils.py +26 -0
  56. 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
+ [![codecov](https://codecov.io/gh/jhnnsrs/fakts/branch/master/graph/badge.svg?token=UGXEA2THBV)](https://codecov.io/gh/jhnnsrs/fakts)
31
+ [![PyPI version](https://badge.fury.io/py/fakts.svg)](https://pypi.org/project/fakts/)
32
+ [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://pypi.org/project/fakts/)
33
+ ![Maintainer](https://img.shields.io/badge/maintainer-jhnnsrs-blue)
34
+ [![PyPI pyversions](https://img.shields.io/pypi/pyversions/fakts.svg)](https://pypi.python.org/pypi/fakts/)
35
+ [![PyPI status](https://img.shields.io/pypi/status/fakts.svg)](https://pypi.python.org/pypi/fakts/)
36
+ [![PyPI download day](https://img.shields.io/pypi/dm/fakts.svg)](https://pypi.python.org/pypi/fakts/)
37
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
38
+ [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
39
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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
+ [![codecov](https://codecov.io/gh/jhnnsrs/fakts/branch/master/graph/badge.svg?token=UGXEA2THBV)](https://codecov.io/gh/jhnnsrs/fakts)
4
+ [![PyPI version](https://badge.fury.io/py/fakts.svg)](https://pypi.org/project/fakts/)
5
+ [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://pypi.org/project/fakts/)
6
+ ![Maintainer](https://img.shields.io/badge/maintainer-jhnnsrs-blue)
7
+ [![PyPI pyversions](https://img.shields.io/pypi/pyversions/fakts.svg)](https://pypi.python.org/pypi/fakts/)
8
+ [![PyPI status](https://img.shields.io/pypi/status/fakts.svg)](https://pypi.python.org/pypi/fakts/)
9
+ [![PyPI download day](https://img.shields.io/pypi/dm/fakts.svg)](https://pypi.python.org/pypi/fakts/)
10
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
11
+ [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
12
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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,11 @@
1
+ import pydantic
2
+ from typing import Dict, Any
3
+ import datetime
4
+
5
+
6
+ class CacheModel(pydantic.BaseModel):
7
+ """Cache file model"""
8
+
9
+ config: Dict[str, Any]
10
+ created: datetime.datetime
11
+ hash: str = ""
@@ -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