PyObservability 1.0.0__py3-none-any.whl → 1.1.0__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.
@@ -32,7 +32,7 @@ def _cli() -> None:
32
32
  )
33
33
  args = [arg.lower() for arg in sys.argv[1:]]
34
34
  try:
35
- assert len(args) > 1
35
+ assert len(args) >= 1
36
36
  except (IndexError, AttributeError, AssertionError):
37
37
  print(f"Cannot proceed without a valid arbitrary command. Please choose from {choices}")
38
38
  exit(1)
@@ -2,5 +2,23 @@ from enum import StrEnum
2
2
 
3
3
 
4
4
  class APIEndpoints(StrEnum):
5
+ """API endpoints for all the routes.
6
+
7
+ >>> APIEndpoints
8
+
9
+ """
10
+
11
+ health = "/health"
5
12
  root = "/"
6
13
  ws = "/ws"
14
+
15
+
16
+ class Log(StrEnum):
17
+ """Log output options.
18
+
19
+ >>> Log
20
+
21
+ """
22
+
23
+ file = "file"
24
+ stdout = "stdout"
@@ -5,12 +5,38 @@ import socket
5
5
  from typing import Any, Dict, List
6
6
 
7
7
  import yaml
8
- from pydantic import BaseModel, Field, FilePath, HttpUrl, PositiveInt
8
+ from pydantic import BaseModel, Field, FilePath, HttpUrl, PositiveInt, DirectoryPath
9
9
  from pydantic.aliases import AliasChoices
10
10
  from pydantic_settings import BaseSettings
11
11
 
12
+ from pyobservability.config import enums
12
13
 
13
- def detailed_log_config() -> Dict[str, Any]:
14
+
15
+ def detailed_log_config(filename: str | None = None, debug: bool = False) -> Dict[str, Any]:
16
+ """Generate a detailed logging configuration.
17
+
18
+ Args:
19
+ filename: Optional log file name. If None, logs to stdout.
20
+ debug: If True, sets log level to DEBUG, else INFO.
21
+
22
+ Returns:
23
+ Dict[str, Any]:
24
+ Returns the logging configuration dictionary.
25
+ """
26
+ if filename:
27
+ log_handler = {
28
+ "class": "logging.FileHandler",
29
+ "formatter": "default",
30
+ "filename": filename,
31
+ "mode": "a",
32
+ }
33
+ else:
34
+ log_handler = {
35
+ "class": "logging.StreamHandler",
36
+ "formatter": "default",
37
+ "stream": "ext://sys.stdout",
38
+ }
39
+ level = "DEBUG" if debug else "INFO"
14
40
  return {
15
41
  "version": 1,
16
42
  "disable_existing_loggers": False,
@@ -20,19 +46,13 @@ def detailed_log_config() -> Dict[str, Any]:
20
46
  "datefmt": "%b-%d-%Y %I:%M:%S %p",
21
47
  }
22
48
  },
23
- "handlers": {
24
- "default": {
25
- "class": "logging.StreamHandler",
26
- "formatter": "default",
27
- "stream": "ext://sys.stdout",
28
- }
29
- },
49
+ "handlers": {"default": log_handler},
30
50
  "loggers": {
31
- "uvicorn": {"handlers": ["default"], "level": "INFO"},
32
- "uvicorn.error": {"handlers": ["default"], "level": "INFO", "propagate": False},
33
- "uvicorn.access": {"handlers": ["default"], "level": "INFO", "propagate": False},
51
+ "uvicorn": {"handlers": ["default"], "level": level},
52
+ "uvicorn.error": {"handlers": ["default"], "level": level, "propagate": False},
53
+ "uvicorn.access": {"handlers": ["default"], "level": level, "propagate": False},
34
54
  },
35
- "root": {"handlers": ["default"], "level": "INFO"},
55
+ "root": {"handlers": ["default"], "level": level},
36
56
  }
37
57
 
38
58
 
@@ -52,11 +72,24 @@ class PydanticEnvConfig(BaseSettings):
52
72
  dotenv_settings,
53
73
  file_secret_settings,
54
74
  ):
55
- """Order: dotenv, env, init, secrets files."""
56
- return dotenv_settings, env_settings, init_settings, file_secret_settings
75
+ """Customize the order of settings sources."""
76
+ # Precedence (last wins):
77
+ # env < dotenv < file secrets < init
78
+ return (
79
+ env_settings,
80
+ dotenv_settings,
81
+ file_secret_settings,
82
+ init_settings,
83
+ )
57
84
 
58
85
 
59
86
  class MonitorTarget(BaseModel):
87
+ """Model representing a monitoring target.
88
+
89
+ >>> MonitorTarget
90
+
91
+ """
92
+
60
93
  name: str
61
94
  base_url: HttpUrl
62
95
  apikey: str
@@ -89,6 +122,9 @@ class EnvConfig(PydanticEnvConfig):
89
122
  targets: List[MonitorTarget] = Field(..., validation_alias=alias_choices("TARGETS"))
90
123
  interval: PositiveInt = Field(3, validation_alias=alias_choices("INTERVAL"))
91
124
 
125
+ log: enums.Log | None = None
126
+ logs_path: str = "logs"
127
+ debug: bool = False
92
128
  log_config: Dict[str, Any] | FilePath | None = None
93
129
 
94
130
  username: str | None = Field(None, validation_alias=alias_choices("USERNAME"))
@@ -99,6 +135,7 @@ class EnvConfig(PydanticEnvConfig):
99
135
 
100
136
  env_prefix = ""
101
137
  extra = "forbid"
138
+ hide_input_in_errors = True
102
139
 
103
140
  @classmethod
104
141
  def from_env_file(cls, filename: pathlib.Path) -> "EnvConfig":
pyobservability/main.py CHANGED
@@ -1,6 +1,9 @@
1
1
  import logging
2
+ import os
2
3
  import pathlib
3
4
  import warnings
5
+ from datetime import datetime
6
+ from typing import Dict
4
7
 
5
8
  import uiauth
6
9
  import uvicorn
@@ -32,23 +35,47 @@ async def index(request: Request):
32
35
 
33
36
  Args:
34
37
  request: FastAPI request object.
38
+
39
+ Returns:
40
+ TemplateResponse:
41
+ Rendered HTML template with targets and version.
35
42
  """
36
43
  return templates.TemplateResponse(
37
44
  "index.html", {"request": request, "targets": settings.env.targets, "version": __version__}
38
45
  )
39
46
 
40
47
 
41
- def include_routes():
48
+ async def health() -> Dict[str, str]:
49
+ """Health check endpoint.
50
+
51
+ Returns:
52
+ dict:
53
+ Health status.
54
+ """
55
+ return {"status": "ok"}
56
+
57
+
58
+ def include_routes() -> None:
59
+ """Include routes in the FastAPI app with or without authentication."""
60
+ PyObservability.routes.append(
61
+ APIRoute(
62
+ path=enums.APIEndpoints.health,
63
+ endpoint=health,
64
+ methods=["GET"],
65
+ include_in_schema=False,
66
+ ),
67
+ )
42
68
  if all((settings.env.username, settings.env.password)):
43
69
  uiauth.protect(
44
70
  app=PyObservability,
45
71
  username=settings.env.username,
46
72
  password=settings.env.password,
73
+ custom_logger=LOGGER,
47
74
  params=[
48
75
  uiauth.Parameters(
49
76
  path=enums.APIEndpoints.root,
50
77
  function=index,
51
- methods=["GET"],
78
+ methods=[uiauth.enums.APIMethods.GET],
52
79
  ),
53
80
  uiauth.Parameters(
54
81
  path=enums.APIEndpoints.ws,
@@ -75,7 +102,8 @@ def include_routes():
75
102
  )
76
103
 
77
104
 
78
- def start(**kwargs):
105
+ def start(**kwargs) -> None:
106
+ """Start the FastAPI app with Uvicorn server."""
79
107
  settings.env = settings.env_loader(**kwargs)
80
108
  settings.env.targets = [{k: str(v) for k, v in target.model_dump().items()} for target in settings.env.targets]
81
109
  settings.targets_by_url = {t["base_url"]: t for t in settings.env.targets}
@@ -85,6 +113,15 @@ def start(**kwargs):
85
113
  port=settings.env.port,
86
114
  app=PyObservability,
87
115
  )
116
+ if settings.env.log:
117
+ if settings.env.log == enums.Log.stdout:
118
+ uvicorn_args["log_config"] = settings.detailed_log_config(debug=settings.env.debug)
119
+ else:
120
+ logs_path = pathlib.Path(settings.env.logs_path)
121
+ log_file = logs_path / f"pyobservability_{datetime.now():%d-%m-%Y}.log"
122
+ logs_path.mkdir(parents=True, exist_ok=True)
123
+ uvicorn_args["log_config"] = settings.detailed_log_config(filename=log_file.resolve(), debug=settings.env.debug)
124
+ # log_config will take precedence if both log and log_config are set
88
125
  if settings.env.log_config:
89
126
  uvicorn_args["log_config"] = (
90
127
  settings.env.log_config if isinstance(settings.env.log_config, dict) else str(settings.env.log_config)
@@ -3,7 +3,7 @@ import json
3
3
  import logging
4
4
  from asyncio import CancelledError
5
5
  from collections.abc import Generator
6
- from typing import Any, Dict, List
6
+ from typing import Any, AsyncGenerator, Dict, List
7
7
 
8
8
  import aiohttp
9
9
 
@@ -14,6 +14,15 @@ OBS_PATH = "/observability"
14
14
 
15
15
 
16
16
  def refine_service(service_list: List[Dict[str, Any]]) -> Generator[Dict[str, Dict[str, str]]]:
17
+ """Refine service stats to only include relevant fields and round CPU values.
18
+
19
+ Args:
20
+ service_list: List of service statistics dictionaries.
21
+
22
+ Yields:
23
+ Dict[str, Dict[str, str]]:
24
+ Refined service statistics dictionary.
25
+ """
17
26
  for service in service_list:
18
27
  service["memory"] = dict(rss=service.get("memory", {}).get("rss"), vms=service.get("memory", {}).get("vms"))
19
28
  service["cpu"] = dict(
@@ -24,7 +33,18 @@ def refine_service(service_list: List[Dict[str, Any]]) -> Generator[Dict[str, Di
24
33
 
25
34
 
26
35
  class Monitor:
36
+ """Monitor class to stream observability data from a target.
37
+
38
+ >>> Monitor
39
+
40
+ """
41
+
27
42
  def __init__(self, target: Dict[str, str]):
43
+ """Initialize Monitor with target configuration.
44
+
45
+ Args:
46
+ target: Dictionary containing target configuration with keys 'name', 'base_url', and 'apikey'.
47
+ """
28
48
  self.name = target["name"]
29
49
  self.base_url = target["base_url"]
30
50
  self.apikey = target["apikey"]
@@ -40,11 +60,22 @@ class Monitor:
40
60
  # SUBSCRIBE / UNSUBSCRIBE
41
61
  # ------------------------------
42
62
  def subscribe(self) -> asyncio.Queue:
63
+ """Subscribe to the monitor's data stream.
64
+
65
+ Returns:
66
+ asyncio.Queue:
67
+ Queue to receive streamed data.
68
+ """
43
69
  q = asyncio.Queue(maxsize=10)
44
70
  self._ws_subscribers.append(q)
45
71
  return q
46
72
 
47
- def unsubscribe(self, q: asyncio.Queue):
73
+ def unsubscribe(self, q: asyncio.Queue) -> None:
74
+ """Unsubscribe from the monitor's data stream.
75
+
76
+ Args:
77
+ q: Queue to be removed from subscribers.
78
+ """
48
79
  if q in self._ws_subscribers:
49
80
  self._ws_subscribers.remove(q)
50
81
 
@@ -52,6 +83,7 @@ class Monitor:
52
83
  # START / STOP
53
84
  # ------------------------------
54
85
  async def start(self):
86
+ """Start the monitor's data streaming."""
55
87
  if self._task:
56
88
  return # already running
57
89
 
@@ -61,6 +93,7 @@ class Monitor:
61
93
  self._task = asyncio.create_task(self._stream_target())
62
94
 
63
95
  async def stop(self):
96
+ """Stop the monitor's data streaming."""
64
97
  self._stop.set()
65
98
  if self._task:
66
99
  self._task.cancel()
@@ -74,7 +107,8 @@ class Monitor:
74
107
  await self.session.close()
75
108
  self.session = None
76
109
 
77
- async def update_flags(self, **kwargs):
110
+ async def update_flags(self, **kwargs) -> None:
111
+ """Update monitor flags and restart the stream."""
78
112
  for k, v in kwargs.items():
79
113
  if k in self.flags:
80
114
  self.flags[k] = v
@@ -82,14 +116,21 @@ class Monitor:
82
116
  # restart stream with new params
83
117
  await self._restart_stream()
84
118
 
85
- async def _restart_stream(self):
119
+ async def _restart_stream(self) -> None:
120
+ """Restart the monitor's data streaming."""
86
121
  await self.stop()
87
122
  await self.start()
88
123
 
89
124
  # ------------------------------
90
125
  # FETCH STREAM
91
126
  # ------------------------------
92
- async def _fetch_stream(self):
127
+ async def _fetch_stream(self) -> AsyncGenerator[Dict[str, Any], None]:
128
+ """Fetch the observability data stream from the target.
129
+
130
+ Yields:
131
+ Dict[str, Any]:
132
+ Parsed observability data.
133
+ """
93
134
  query = f"?interval={settings.env.interval}"
94
135
  if self.flags["all_services"]:
95
136
  query += "&all_services=true"
@@ -122,7 +163,8 @@ class Monitor:
122
163
  # ------------------------------
123
164
  # STREAM LOOP
124
165
  # ------------------------------
125
- async def _stream_target(self):
166
+ async def _stream_target(self) -> None:
167
+ """Stream observability data from the target and notify subscribers."""
126
168
  errors = {}
127
169
  while not self._stop.is_set():
128
170
  try:
@@ -10,13 +10,24 @@ from pyobservability.monitor import Monitor
10
10
  LOGGER = logging.getLogger("uvicorn.default")
11
11
 
12
12
 
13
- async def _forward_metrics(websocket: WebSocket, q: asyncio.Queue):
13
+ async def _forward_metrics(websocket: WebSocket, q: asyncio.Queue) -> None:
14
+ """Forward metrics from the monitor's queue to the websocket.
15
+
16
+ Args:
17
+ websocket: FastAPI WebSocket connection.
18
+ q: asyncio.Queue to receive metrics from the monitor.
19
+ """
14
20
  while True:
15
21
  payload = await q.get()
16
22
  await websocket.send_json(payload)
17
23
 
18
24
 
19
- async def websocket_endpoint(websocket: WebSocket):
25
+ async def websocket_endpoint(websocket: WebSocket) -> None:
26
+ """Websocket endpoint to handle observability data streaming.
27
+
28
+ Args:
29
+ websocket: FastAPI WebSocket connection.
30
+ """
20
31
  await websocket.accept()
21
32
 
22
33
  monitor: Monitor | None = None
@@ -1 +1 @@
1
- __version__ = "1.0.0"
1
+ __version__ = "1.1.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyObservability
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Lightweight OS-agnostic observability UI for PyNinja
5
5
  Author-email: Vignesh Rao <svignesh1793@gmail.com>
6
6
  License: MIT License
@@ -29,11 +29,11 @@ Project-URL: Homepage, https://github.com/thevickypedia/PyObservability
29
29
  Project-URL: Docs, https://thevickypedia.github.io/PyObservability
30
30
  Project-URL: Source, https://github.com/thevickypedia/PyObservability
31
31
  Project-URL: Bug Tracker, https://github.com/thevickypedia/PyObservability/issues
32
- Project-URL: Release Notes, https://github.com/thevickypedia/PyObservability/blob/main/release_notes.rst
32
+ Project-URL: Release Notes, https://github.com/thevickypedia/PyObservability/blob/main/release_notes.md
33
33
  Keywords: PyObservability,observability,system-monitor,PyNinja
34
34
  Classifier: License :: OSI Approved :: MIT License
35
35
  Classifier: Programming Language :: Python :: 3
36
- Classifier: Development Status :: 3 - Alpha
36
+ Classifier: Development Status :: 5 - Production/Stable
37
37
  Classifier: Operating System :: MacOS :: MacOS X
38
38
  Classifier: Operating System :: Microsoft :: Windows
39
39
  Classifier: Operating System :: POSIX :: Linux
@@ -50,10 +50,7 @@ Requires-Dist: pydantic-settings==2.12.*
50
50
  Requires-Dist: python-dotenv==1.2.*
51
51
  Requires-Dist: uvicorn[standard]==0.38.*
52
52
  Provides-Extra: dev
53
- Requires-Dist: sphinx==5.1.1; extra == "dev"
54
53
  Requires-Dist: pre-commit; extra == "dev"
55
- Requires-Dist: recommonmark; extra == "dev"
56
- Requires-Dist: gitverse; extra == "dev"
57
54
  Dynamic: license-file
58
55
 
59
56
  # PyObservability
@@ -69,6 +66,7 @@ Dynamic: license-file
69
66
  [![pypi][label-actions-pypi]][gha_pypi]
70
67
  [![notes][label-actions-notes]][gha_notes]
71
68
  [![release][label-actions-release]][gha_release]
69
+ [![docker][label-actions-docker]][gha_docker]
72
70
 
73
71
  [![Pypi][label-pypi]][pypi]
74
72
  [![Pypi-format][label-pypi-format]][pypi-files]
@@ -102,13 +100,25 @@ pyobservability start
102
100
 
103
101
  > Use `pyobservability --help` for usage instructions.
104
102
 
103
+ **Containerized Deployment**
104
+ ```shell
105
+ docker pull thevickypedia/pyobservability:latest
106
+
107
+ docker run \
108
+ --name observability \
109
+ -p 8080:80 \
110
+ -v /home/user/config:/config \
111
+ --restart=no \
112
+ thevickypedia/pyobservability
113
+ ```
114
+
105
115
  ## Environment Variables
106
116
 
107
117
  <details>
108
118
  <summary><strong>Sourcing environment variables from an env file</strong></summary>
109
119
 
110
120
  > _By default, `PyObservability` will look for a `.env` file in the current working directory._
111
- > _Other file options are supported with a custom kwarg or env var `env_file` pointing to the filepath._
121
+ > _Other file options (like JSON and YAML) are supported with a custom kwarg or env var `env_file` pointing to the filepath._
112
122
  </details>
113
123
 
114
124
  **Mandatory**
@@ -142,22 +152,14 @@ Licensed under the [MIT License][license]
142
152
  [label-pypi-status]: https://img.shields.io/pypi/status/PyObservability
143
153
  [label-actions-notes]: https://github.com/thevickypedia/PyObservability/actions/workflows/notes.yml/badge.svg
144
154
  [label-actions-release]: https://github.com/thevickypedia/PyObservability/actions/workflows/release.yml/badge.svg
155
+ [label-actions-docker]: https://github.com/thevickypedia/PyObservability/actions/workflows/docker.yml/badge.svg
145
156
 
146
157
  [3.11]: https://docs.python.org/3/whatsnew/3.11.html
147
158
  [virtual environment]: https://docs.python.org/3/tutorial/venv.html
148
- [release-notes]: https://github.com/thevickypedia/PyObservability/blob/main/release_notes.rst
149
159
  [gha_pypi]: https://github.com/thevickypedia/PyObservability/actions/workflows/python-publish.yml
150
160
  [gha_notes]: https://github.com/thevickypedia/PyObservability/actions/workflows/notes.yml
151
161
  [gha_release]: https://github.com/thevickypedia/PyObservability/actions/workflows/release.yml
152
- [google-docs]: https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings
153
- [pep8]: https://www.python.org/dev/peps/pep-0008/
154
- [isort]: https://pycqa.github.io/isort/
155
- [sphinx]: https://www.sphinx-doc.org/en/master/man/sphinx-autogen.html
162
+ [gha_docker]: https://github.com/thevickypedia/PyObservability/actions/workflows/docker.yml
156
163
  [pypi]: https://pypi.org/project/PyObservability
157
164
  [pypi-files]: https://pypi.org/project/PyObservability/#files
158
- [pypi-repo]: https://packaging.python.org/tutorials/packaging-projects/
159
165
  [license]: https://github.com/thevickypedia/PyObservability/blob/main/LICENSE
160
- [runbook]: https://thevickypedia.github.io/PyObservability/
161
- [samples]: https://github.com/thevickypedia/PyObservability/tree/main/samples
162
- [PyUdisk]: https://github.com/thevickypedia/PyUdisk
163
- [PyArchitecture]: https://github.com/thevickypedia/PyArchitecture
@@ -0,0 +1,16 @@
1
+ pyobservability/__init__.py,sha256=yVBLyTohBiBKp0Otyl04IggPh8mhg3Er25u6eFyxMto,2618
2
+ pyobservability/main.py,sha256=WC57zDTv588aNPViV352WcWmJxURF5EvY9X26roNuW0,4316
3
+ pyobservability/monitor.py,sha256=Y38zDJFPDbQqz_2jQcNmJBnRpeobC_SV2uhN-13e9RU,7591
4
+ pyobservability/transport.py,sha256=zHLAodX20bKkPOuacKjzv1Dqj3JbNAB75o1ABwzum0U,2534
5
+ pyobservability/version.py,sha256=LGVQyDsWifdACo7qztwb8RWWHds1E7uQ-ZqD8SAjyw4,22
6
+ pyobservability/config/enums.py,sha256=EhvD9kB5EMW3ARxr5KmISmf-rP3D4IKqOIjw6Tb8SB8,294
7
+ pyobservability/config/settings.py,sha256=adeTQCV5_bf_8zBggkdIXniyXsbqJYAAUupB2tvOVNI,5820
8
+ pyobservability/static/app.js,sha256=6hjFy2jt4ndYJUI1DZT08CpuxHyWj9iSAZ2vxaTqH2A,20664
9
+ pyobservability/static/styles.css,sha256=dnYSXNeXd6Ohu4h8sJCO87vzPbkYcw9XVGGwB8IJbxw,4703
10
+ pyobservability/templates/index.html,sha256=2aQdb0QlDY5Hboev-_lJPlpnGxiC6h3fp0FlvL72S9k,5227
11
+ pyobservability-1.1.0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
12
+ pyobservability-1.1.0.dist-info/METADATA,sha256=KZoudD6ox48fOOmhPEW7WXPgYO26bps3C9kq68wkFy0,6539
13
+ pyobservability-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ pyobservability-1.1.0.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
15
+ pyobservability-1.1.0.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
16
+ pyobservability-1.1.0.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- pyobservability/__init__.py,sha256=rr4udGMbbNPl3yo7l8R3FUUVVahBtYVaW6vSWWgXlv0,2617
2
- pyobservability/main.py,sha256=Ty0bS7ZWyKXuo-xuKoFQESvT-B-19W04d_Nk0KveNtg,3004
3
- pyobservability/monitor.py,sha256=4Xd8k7gcOmHM-WvQpgFiDVGtzMu_RpFPZXPPoz4GoA4,6224
4
- pyobservability/transport.py,sha256=FyzJAMZPn7JUZIGgxnSw3on1K6T4ciZE1EuGdAcxt_w,2188
5
- pyobservability/version.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
6
- pyobservability/config/enums.py,sha256=iMIOpa8LYSszkPIYBhupX--KrEXVTTsBurinpAxLvMA,86
7
- pyobservability/config/settings.py,sha256=BjTZnkGS1pZKbN3Fq_5HQX8C6oEjWn_gVJLXcZZ6EWE,4853
8
- pyobservability/static/app.js,sha256=6hjFy2jt4ndYJUI1DZT08CpuxHyWj9iSAZ2vxaTqH2A,20664
9
- pyobservability/static/styles.css,sha256=dnYSXNeXd6Ohu4h8sJCO87vzPbkYcw9XVGGwB8IJbxw,4703
10
- pyobservability/templates/index.html,sha256=2aQdb0QlDY5Hboev-_lJPlpnGxiC6h3fp0FlvL72S9k,5227
11
- pyobservability-1.0.0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
12
- pyobservability-1.0.0.dist-info/METADATA,sha256=4Wj2FH4KYhsDbEntXxnMginYE9QR9AluZyYLMHvwhbg,6822
13
- pyobservability-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- pyobservability-1.0.0.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
15
- pyobservability-1.0.0.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
16
- pyobservability-1.0.0.dist-info/RECORD,,