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.
@@ -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
@@ -0,0 +1,10 @@
1
+ from typing import TypeVar, Type
2
+
3
+
4
+ StringEnumType = TypeVar('StringEnumType', bound='StringEnum')
5
+
6
+
7
+ class StringEnum:
8
+ @classmethod
9
+ def __getitem__(cls: Type[StringEnumType], item: str) -> StringEnumType:
10
+ return getattr(cls, item.upper())
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
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any