pfun-common 0.1.28__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.
- pfun_common/__init__.py +28 -0
- pfun_common/enums.py +10 -0
- pfun_common/plot.py +12 -0
- pfun_common/settings.py +139 -0
- pfun_common/utils.py +109 -0
- pfun_common-0.1.28.dist-info/METADATA +9 -0
- pfun_common-0.1.28.dist-info/RECORD +8 -0
- pfun_common-0.1.28.dist-info/WHEEL +4 -0
pfun_common/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"settings",
|
|
5
|
+
"Settings",
|
|
6
|
+
"get_settings",
|
|
7
|
+
"utils",
|
|
8
|
+
"load_environment_variables",
|
|
9
|
+
"setup_logging"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import pfun_common.pfun_common.settings as settings
|
|
14
|
+
import pfun_common.pfun_common.utils as utils
|
|
15
|
+
from pfun_common.pfun_common import (
|
|
16
|
+
load_environment_variables,
|
|
17
|
+
setup_logging
|
|
18
|
+
)
|
|
19
|
+
except (ImportError, ModuleNotFoundError):
|
|
20
|
+
import pfun_common.settings as settings
|
|
21
|
+
from pfun_common.settings import (
|
|
22
|
+
Settings, get_settings
|
|
23
|
+
)
|
|
24
|
+
import pfun_common.utils as utils
|
|
25
|
+
from pfun_common.utils import (
|
|
26
|
+
load_environment_variables,
|
|
27
|
+
setup_logging
|
|
28
|
+
)
|
pfun_common/enums.py
ADDED
pfun_common/plot.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
from matplotlib.axes import Axes
|
|
3
|
+
import seaborn as sns
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import logging
|
|
6
|
+
logger = logging.getLogger("pfun_cma_model")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def lineplot(df: pd.DataFrame, tcol='ts_local', ycol='sg') -> Axes:
|
|
10
|
+
"""Quality-of-life lineplot function for quick n dirty plots of glucose."""
|
|
11
|
+
axes = sns.lineplot(df, x=tcol, y=ycol)
|
|
12
|
+
return axes
|
pfun_common/settings.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""pfun_common settings module."""
|
|
2
|
+
import logging
|
|
3
|
+
from base64 import b64encode
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
from secrets import token_urlsafe
|
|
7
|
+
|
|
8
|
+
from pydantic import field_validator, Field
|
|
9
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_default_secret_key() -> str:
|
|
13
|
+
"""Generate a default secret key based on the current timestamp.
|
|
14
|
+
|
|
15
|
+
Note: This is not secure and should only be used for development purposes.
|
|
16
|
+
In production, set the SECRET_KEY environment variable to a secure value.
|
|
17
|
+
"""
|
|
18
|
+
timestamp = datetime.now().isoformat().encode("utf-8")
|
|
19
|
+
timestamp_nonce = b64encode(timestamp).decode("utf-8")
|
|
20
|
+
rand_token = token_urlsafe(16) # 16 bytes of randomness
|
|
21
|
+
return f"{timestamp_nonce}-{rand_token}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Settings(BaseSettings):
|
|
25
|
+
"""Application settings"""
|
|
26
|
+
|
|
27
|
+
debug: bool = False
|
|
28
|
+
server_scheme: str = "http"
|
|
29
|
+
server_host: str = "localhost"
|
|
30
|
+
server_port: str | int = "8001"
|
|
31
|
+
gradio_server_scheme: str = "http"
|
|
32
|
+
gradio_server_host: str = "localhost"
|
|
33
|
+
gradio_server_port: str | int = "7860"
|
|
34
|
+
redis_user: str = "default"
|
|
35
|
+
redis_password: str = ""
|
|
36
|
+
redis_host: str = "localhost"
|
|
37
|
+
redis_port: str | int = "6379"
|
|
38
|
+
redis_db: str | int | bool = "0"
|
|
39
|
+
redis_connection_string: str = ""
|
|
40
|
+
perplexity_api_key: str = ""
|
|
41
|
+
secret_key: str = Field(default_factory=lambda: generate_default_secret_key())
|
|
42
|
+
google_cloud_project_id: str = "pfun-cma-model"
|
|
43
|
+
google_cloud_location: str = "us-central1"
|
|
44
|
+
|
|
45
|
+
model_config = SettingsConfigDict(
|
|
46
|
+
case_sensitive=False,
|
|
47
|
+
env_file=(".env",),
|
|
48
|
+
env_file_encoding="utf-8",
|
|
49
|
+
extra="allow",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@field_validator("redis_connection_string", mode="after")
|
|
53
|
+
@classmethod
|
|
54
|
+
def parse_redis_connection_string(cls, v: str, info) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Parse REDIS_CONNECTION_STRING and override individual Redis settings.
|
|
57
|
+
|
|
58
|
+
Supports URLs in the format: redis://[user[:password]@]host[:port][/db]
|
|
59
|
+
"""
|
|
60
|
+
if not v:
|
|
61
|
+
return v
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# initially, strip any surrounding whitespace
|
|
65
|
+
v = v.strip()
|
|
66
|
+
# somewhat intelligently access the URL itself (without extra params)
|
|
67
|
+
v = [piece for piece in v.split(" ") if "redis://" in piece][0].strip()
|
|
68
|
+
logging.debug("Parsing REDIS_CONNECTION_STRING: %s", v)
|
|
69
|
+
|
|
70
|
+
# parse the URL
|
|
71
|
+
parsed = urlparse(v)
|
|
72
|
+
logging.debug("Parsed Redis URL: %s", parsed)
|
|
73
|
+
|
|
74
|
+
# Extract host (required)
|
|
75
|
+
if parsed.hostname:
|
|
76
|
+
logging.debug("Parsed Redis host: %s", parsed.hostname)
|
|
77
|
+
info.data["redis_host"] = parsed.hostname
|
|
78
|
+
|
|
79
|
+
# Extract port (optional, defaults to 6379)
|
|
80
|
+
if parsed.port:
|
|
81
|
+
info.data["redis_port"] = parsed.port
|
|
82
|
+
elif parsed.hostname: # Only set default if we have a hostname
|
|
83
|
+
info.data["redis_port"] = 6379
|
|
84
|
+
|
|
85
|
+
# Extract username (optional, defaults to "default")
|
|
86
|
+
if parsed.username:
|
|
87
|
+
info.data["redis_user"] = parsed.username
|
|
88
|
+
|
|
89
|
+
# Extract password (optional)
|
|
90
|
+
if parsed.password:
|
|
91
|
+
info.data["redis_password"] = parsed.password
|
|
92
|
+
|
|
93
|
+
# Extract database number from path (optional, e.g., "/0")
|
|
94
|
+
if parsed.path and parsed.path != "/":
|
|
95
|
+
db_str = parsed.path.lstrip("/")
|
|
96
|
+
if db_str:
|
|
97
|
+
try:
|
|
98
|
+
info.data["redis_db"] = int(db_str)
|
|
99
|
+
except ValueError:
|
|
100
|
+
pass # Keep existing value if db is not a valid integer
|
|
101
|
+
|
|
102
|
+
logging.debug("Parsed Redis settings: host=%s, port=%s, user=%s, db=%s",
|
|
103
|
+
info.data.get("redis_host"),
|
|
104
|
+
info.data.get("redis_port"),
|
|
105
|
+
info.data.get("redis_user"),
|
|
106
|
+
info.data.get("redis_db"))
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
logging.warning("Failed to parse REDIS_CONNECTION_STRING: %s", v, exc_info=exc)
|
|
109
|
+
logging.debug("No such REDIS_CONNECTION_STRING: %s", v, exc_info=exc)
|
|
110
|
+
pass # Keep existing values if parsing fails
|
|
111
|
+
|
|
112
|
+
return v
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def llm_gen_scenario_endpoint(self) -> str:
|
|
116
|
+
"""
|
|
117
|
+
LLM generate-scenario endpoint URL.
|
|
118
|
+
|
|
119
|
+
:param self: Description
|
|
120
|
+
:return: Description
|
|
121
|
+
:rtype: str
|
|
122
|
+
"""
|
|
123
|
+
return f"{self.server_scheme}://{self.server_host}:{self.server_port}/llm/generate-scenario"
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def gradio_demo_endpoint(self) -> str:
|
|
127
|
+
"""
|
|
128
|
+
Gradio demo endpoint URL.
|
|
129
|
+
|
|
130
|
+
:param self: Description
|
|
131
|
+
:return: Description
|
|
132
|
+
:rtype: str
|
|
133
|
+
"""
|
|
134
|
+
return f"{self.gradio_server_scheme}://{self.gradio_server_host}:{self.gradio_server_port}/gradio/"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_settings() -> Settings:
|
|
138
|
+
"""Initialize the settings object (dependency injection helper method)."""
|
|
139
|
+
return Settings()
|
pfun_common/utils.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from json import dumps
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
# Python 2 fallback
|
|
11
|
+
from urllib import urlencode, unquote # type: ignore
|
|
12
|
+
from urlparse import urlparse, parse_qsl, ParseResult # type: ignore
|
|
13
|
+
except ImportError:
|
|
14
|
+
# Python 3 fallback
|
|
15
|
+
from urllib.parse import (
|
|
16
|
+
urlencode, unquote, urlparse, parse_qsl, ParseResult
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def setup_logging(logger: logging.Logger, debug_mode: bool = False):
|
|
21
|
+
"""Setup logging configuration."""
|
|
22
|
+
# Set the logger to the desired level
|
|
23
|
+
if debug_mode:
|
|
24
|
+
logger.setLevel(logging.DEBUG)
|
|
25
|
+
logger.debug("Debug mode is enabled. Setting logger level to DEBUG.")
|
|
26
|
+
else:
|
|
27
|
+
logger.setLevel(logging.INFO)
|
|
28
|
+
logger.info("Debug mode is disabled. Setting logger level to INFO.")
|
|
29
|
+
logger.info("...Logging setup complete.")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_environment_variables(
|
|
33
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
34
|
+
) -> tuple[bool, Path]:
|
|
35
|
+
"""Load environment variables from .env file."""
|
|
36
|
+
logger.debug("Attempting to load environment variables from .env file...")
|
|
37
|
+
env_file = Path(__file__).parent.parent.parent / ".env"
|
|
38
|
+
logger.debug("Checking for .env file at: %s", str(env_file))
|
|
39
|
+
if not env_file.exists():
|
|
40
|
+
logger.warning(
|
|
41
|
+
"No .env file found at '%s'. Using system environment variables.",
|
|
42
|
+
str(env_file))
|
|
43
|
+
return False, env_file
|
|
44
|
+
logger.debug("...env file exists.")
|
|
45
|
+
loaded = load_dotenv(dotenv_path=env_file)
|
|
46
|
+
if not loaded:
|
|
47
|
+
logger.warning(
|
|
48
|
+
f"Failed to load environment variables from {env_file}.")
|
|
49
|
+
logger.debug(f"Loaded environment variables from {env_file}")
|
|
50
|
+
return loaded, env_file
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def add_url_params(url, params):
|
|
55
|
+
""" Add GET params to provided URL being aware of existing.
|
|
56
|
+
|
|
57
|
+
:param url: string of target URL
|
|
58
|
+
:param params: dict containing requested params to be added
|
|
59
|
+
:return: string with updated URL
|
|
60
|
+
|
|
61
|
+
ref: https://stackoverflow.com/a/25580545/1871569
|
|
62
|
+
|
|
63
|
+
>> url = 'https://stackoverflow.com/test?answers=true'
|
|
64
|
+
>> new_params = {'answers': False, 'data': ['some','values']}
|
|
65
|
+
>> add_url_params(url, new_params)
|
|
66
|
+
'https://stackoverflow.com/test?data=some&data=values&answers=false'
|
|
67
|
+
"""
|
|
68
|
+
# Unquoting URL first so we don't lose existing args
|
|
69
|
+
url = unquote(url)
|
|
70
|
+
# Extracting url info
|
|
71
|
+
parsed_url = urlparse(url)
|
|
72
|
+
# Extracting URL arguments from parsed URL
|
|
73
|
+
get_args = parsed_url.query
|
|
74
|
+
# Converting URL arguments to dict
|
|
75
|
+
parsed_get_args = dict(parse_qsl(get_args))
|
|
76
|
+
# Merging URL arguments dict with new params
|
|
77
|
+
parsed_get_args.update(params)
|
|
78
|
+
|
|
79
|
+
# Bool and Dict values should be converted to json-friendly values
|
|
80
|
+
# you may throw this part away if you don't like it :)
|
|
81
|
+
parsed_get_args.update(
|
|
82
|
+
{k: dumps(v) for k, v in parsed_get_args.items()
|
|
83
|
+
if isinstance(v, (bool, dict))}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Converting URL argument to proper query string
|
|
87
|
+
encoded_get_args = urlencode(parsed_get_args, doseq=True)
|
|
88
|
+
# Creating new parsed result object based on provided with new
|
|
89
|
+
# URL arguments. Same thing happens inside urlparse.
|
|
90
|
+
new_url = ParseResult(
|
|
91
|
+
parsed_url.scheme, parsed_url.netloc, parsed_url.path,
|
|
92
|
+
parsed_url.params, encoded_get_args, parsed_url.fragment
|
|
93
|
+
).geturl()
|
|
94
|
+
|
|
95
|
+
return new_url
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def append_root_path():
|
|
99
|
+
"""
|
|
100
|
+
adds the root path to the python path
|
|
101
|
+
"""
|
|
102
|
+
#: pfun imports (relative)
|
|
103
|
+
root_path = str(Path(__file__).parents[1])
|
|
104
|
+
mod_path = str(Path(__file__).parent)
|
|
105
|
+
if root_path not in sys.path:
|
|
106
|
+
sys.path.insert(0, root_path)
|
|
107
|
+
if mod_path not in sys.path:
|
|
108
|
+
sys.path.insert(0, mod_path)
|
|
109
|
+
return sys.path
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pfun-common
|
|
3
|
+
Version: 0.1.28
|
|
4
|
+
Summary: Common utilities and functions for PFun projects.
|
|
5
|
+
Author-email: Robbie Capps <robbie@pfun.me>
|
|
6
|
+
Requires-Python: ==3.12.11
|
|
7
|
+
Requires-Dist: matplotlib>=3.8.0
|
|
8
|
+
Requires-Dist: pandas>=2.2.0
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.2.1
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pfun_common/__init__.py,sha256=5gTRGLWWpT7LTv7kJkh9DJGuDiNGsxvq4mC3Cfo6a-k,662
|
|
2
|
+
pfun_common/enums.py,sha256=yfUzPJmtFpIPJ-QJ7PTVsVmt_xMum3FaDvZ5CDM1w3k,253
|
|
3
|
+
pfun_common/plot.py,sha256=41TbtmhAp4swIl75_ub1GGl0D38LdyviU5q_YYRc6pg,377
|
|
4
|
+
pfun_common/settings.py,sha256=WCVYi9PpvllE0zqjLLnSvxPthqT3lbguWJMibausYg8,5019
|
|
5
|
+
pfun_common/utils.py,sha256=cKyrOGaXPyuJGIV3KMCMwveWtfAq7bE3JUghCJc0Oy0,3767
|
|
6
|
+
pfun_common-0.1.28.dist-info/METADATA,sha256=ipXvBP0d2iHp9oBbHllHj0sHt64H5PWMhqNmj8Z6xcQ,288
|
|
7
|
+
pfun_common-0.1.28.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
+
pfun_common-0.1.28.dist-info/RECORD,,
|