dStats 0.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
dStats/__init__.py ADDED
File without changes
dStats/asgi.py ADDED
@@ -0,0 +1,16 @@
1
+ """
2
+ ASGI config for dStats project.
3
+
4
+ It exposes the ASGI callable as a module-level variable named ``application``.
5
+
6
+ For more information on this file, see
7
+ https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
8
+ """
9
+
10
+ import os
11
+
12
+ from django.core.asgi import get_asgi_application
13
+
14
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dStats.settings")
15
+
16
+ application = get_asgi_application()
dStats/settings.py ADDED
@@ -0,0 +1,128 @@
1
+ """
2
+ Django settings for dStats project.
3
+
4
+ Generated by 'django-admin startproject' using Django 5.1.4.
5
+
6
+ For more information on this file, see
7
+ https://docs.djangoproject.com/en/5.1/topics/settings/
8
+
9
+ For the full list of settings and their values, see
10
+ https://docs.djangoproject.com/en/5.1/ref/settings/
11
+ """
12
+
13
+ from pathlib import Path
14
+
15
+ # Build paths inside the project like this: BASE_DIR / 'subdir'.
16
+ BASE_DIR = Path(__file__).resolve().parent.parent
17
+
18
+
19
+ # Quick-start development settings - unsuitable for production
20
+ # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
21
+
22
+ # SECURITY WARNING: keep the secret key used in production secret!
23
+ SECRET_KEY = "django-insecure-z)f1y6v3$3fa1=pbz2_mv&uhx=#cne(@=vy*)$1j-h(fyit+ri"
24
+
25
+ # SECURITY WARNING: don't run with debug turned on in production!
26
+ DEBUG = True
27
+
28
+ ALLOWED_HOSTS = ["*"]
29
+
30
+
31
+ # Application definition
32
+
33
+ INSTALLED_APPS = [
34
+ "daphne",
35
+ "django.contrib.admin",
36
+ "django.contrib.auth",
37
+ "django.contrib.contenttypes",
38
+ "django.contrib.sessions",
39
+ "django.contrib.messages",
40
+ "django.contrib.staticfiles",
41
+ "dStats",
42
+ ]
43
+
44
+ MIDDLEWARE = [
45
+ "django.middleware.security.SecurityMiddleware",
46
+ "django.contrib.sessions.middleware.SessionMiddleware",
47
+ "django.middleware.common.CommonMiddleware",
48
+ "django.middleware.csrf.CsrfViewMiddleware",
49
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
50
+ "django.contrib.messages.middleware.MessageMiddleware",
51
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
52
+ ]
53
+
54
+ ROOT_URLCONF = "dStats.urls"
55
+
56
+ TEMPLATES = [
57
+ {
58
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
59
+ "DIRS": [],
60
+ "APP_DIRS": True,
61
+ "OPTIONS": {
62
+ "context_processors": [
63
+ "django.template.context_processors.debug",
64
+ "django.template.context_processors.request",
65
+ "django.contrib.auth.context_processors.auth",
66
+ "django.contrib.messages.context_processors.messages",
67
+ ],
68
+ },
69
+ },
70
+ ]
71
+
72
+ # WSGI_APPLICATION = "dStats.wsgi.application"
73
+ ASGI_APPLICATION = "dStats.asgi.application"
74
+
75
+
76
+ # Database
77
+ # https://docs.djangoproject.com/en/5.1/ref/settings/#databases
78
+
79
+ DATABASES = {
80
+ "default": {
81
+ "ENGINE": "django.db.backends.sqlite3",
82
+ "NAME": BASE_DIR / "db.sqlite3",
83
+ }
84
+ }
85
+ MIGRATION_MODULES = {
86
+ "dStats": None, # Disable migrations for dStats app
87
+ }
88
+
89
+ # Password validation
90
+ # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
91
+
92
+ AUTH_PASSWORD_VALIDATORS = [
93
+ {
94
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
95
+ },
96
+ {
97
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
98
+ },
99
+ {
100
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
101
+ },
102
+ {
103
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
104
+ },
105
+ ]
106
+
107
+
108
+ # Internationalization
109
+ # https://docs.djangoproject.com/en/5.1/topics/i18n/
110
+
111
+ LANGUAGE_CODE = "en-us"
112
+
113
+ TIME_ZONE = "UTC"
114
+
115
+ USE_I18N = True
116
+
117
+ USE_TZ = True
118
+
119
+
120
+ # Static files (CSS, JavaScript, Images)
121
+ # https://docs.djangoproject.com/en/5.1/howto/static-files/
122
+
123
+ STATIC_URL = "static/"
124
+
125
+ # Default primary key field type
126
+ # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
127
+
128
+ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
dStats/urls.py ADDED
@@ -0,0 +1,24 @@
1
+ """
2
+ URL configuration for dStats project.
3
+
4
+ The `urlpatterns` list routes URLs to views. For more information please see:
5
+ https://docs.djangoproject.com/en/5.1/topics/http/urls/
6
+ Examples:
7
+ Function views
8
+ 1. Add an import: from my_app import views
9
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
10
+ Class-based views
11
+ 1. Add an import: from other_app.views import Home
12
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13
+ Including another URLconf
14
+ 1. Import the include() function: from django.urls import include, path
15
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16
+ """
17
+
18
+ from django.contrib import admin
19
+ from django.urls import path
20
+ from .views import DockerStatsView
21
+
22
+ urlpatterns = [
23
+ path("", DockerStatsView.as_view(), name="stats"),
24
+ ]
dStats/views.py ADDED
@@ -0,0 +1,308 @@
1
+ # views.py
2
+ import docker
3
+ import json
4
+ from dataclasses import asdict, dataclass
5
+ from typing import List, Dict
6
+ import random
7
+ from graphviz import Graph
8
+ import base64
9
+ from io import BytesIO
10
+ from django.views.generic import TemplateView
11
+ from django.http import JsonResponse
12
+ from django.utils.decorators import method_decorator
13
+ from django.views.decorators.csrf import csrf_exempt
14
+
15
+
16
+ # Keep the existing dataclass definitions
17
+ @dataclass
18
+ class Network:
19
+ name: str
20
+ gateway: str
21
+ internal: bool
22
+ isolated: bool
23
+ color: str
24
+
25
+
26
+ @dataclass
27
+ class Interface:
28
+ endpoint_id: str
29
+ address: str
30
+ aliases: List[str]
31
+
32
+
33
+ @dataclass
34
+ class PortMapping:
35
+ internal: int
36
+ external: int
37
+ protocol: str
38
+
39
+
40
+ @dataclass
41
+ class Container:
42
+ container_id: str
43
+ name: str
44
+ interfaces: List[Interface]
45
+ ports: List[PortMapping]
46
+
47
+
48
+ @dataclass
49
+ class Link:
50
+ container_id: str
51
+ endpoint_id: str
52
+ network_name: str
53
+
54
+
55
+ # Keep the existing color definitions
56
+ COLORS = [
57
+ "#1f78b4",
58
+ "#33a02c",
59
+ "#e31a1c",
60
+ "#ff7f00",
61
+ "#6a3d9a",
62
+ "#b15928",
63
+ "#a6cee3",
64
+ "#b2df8a",
65
+ "#fdbf6f",
66
+ "#cab2d6",
67
+ "#ffff99",
68
+ ]
69
+ color_index = 0
70
+
71
+
72
+ def get_unique_color() -> str:
73
+ global color_index
74
+ if color_index < len(COLORS):
75
+ c = COLORS[color_index]
76
+ color_index += 1
77
+ else:
78
+ c = "#%06x" % random.randint(0, 0xFFFFFF)
79
+ return c
80
+
81
+
82
+ class DockerStatsView(TemplateView):
83
+ template_name = "dStats/index.html"
84
+
85
+ def get_context_data(self, **kwargs):
86
+ context = super().get_context_data(**kwargs)
87
+ return context
88
+
89
+ def generate_network_graph(self):
90
+ client = docker.from_env()
91
+ networks = self.get_networks(client)
92
+ containers, links = self.get_containers(client)
93
+
94
+ g = Graph(
95
+ comment="Docker Network Graph",
96
+ engine="sfdp",
97
+ format="png",
98
+ graph_attr={
99
+ "splines": "true",
100
+ "overlap": "false",
101
+ "nodesep": "2.0",
102
+ "ranksep": "2.0",
103
+ },
104
+ )
105
+
106
+ # Draw networks and containers
107
+ for network in networks.values():
108
+ self.draw_network(g, network)
109
+
110
+ for container in containers:
111
+ self.draw_container(g, container)
112
+
113
+ for link in links:
114
+ if link.network_name != "none":
115
+ self.draw_link(g, networks, link)
116
+
117
+ # Convert graph to base64 image
118
+ img_data = g.pipe()
119
+ encoded_img = base64.b64encode(img_data).decode("utf-8")
120
+ return encoded_img
121
+
122
+ def get_networks(self, client):
123
+ networks = {}
124
+ for net in sorted(client.networks.list(), key=lambda k: k.name):
125
+ try:
126
+ config = net.attrs["IPAM"]["Config"]
127
+ gateway = config[0]["Subnet"] if config else "0.0.0.0"
128
+ except (KeyError, IndexError):
129
+ continue
130
+
131
+ internal = net.attrs.get("Internal", False)
132
+ isolated = (
133
+ net.attrs.get("Options", {}).get(
134
+ "com.docker.network.bridge.enable_icc", "true"
135
+ )
136
+ == "false"
137
+ )
138
+
139
+ color = get_unique_color()
140
+ networks[net.name] = Network(net.name, gateway, internal, isolated, color)
141
+
142
+ networks["host"] = Network("host", "0.0.0.0", False, False, "#808080")
143
+ return networks
144
+
145
+ def get_containers(self, client):
146
+ containers = []
147
+ links = []
148
+
149
+ for container in client.containers.list():
150
+ interfaces = []
151
+ ports = []
152
+
153
+ for net_name, net_info in container.attrs["NetworkSettings"][
154
+ "Networks"
155
+ ].items():
156
+ endpoint_id = net_info["EndpointID"]
157
+ aliases = net_info.get("Aliases", [])
158
+ interfaces.append(
159
+ Interface(endpoint_id, net_info["IPAddress"], aliases)
160
+ )
161
+ links.append(Link(container.id, endpoint_id, net_name))
162
+
163
+ port_mappings = container.attrs["NetworkSettings"]["Ports"]
164
+ if port_mappings:
165
+ for container_port, host_ports in port_mappings.items():
166
+ if host_ports:
167
+ for host_port in host_ports:
168
+ internal_port, protocol = container_port.split("/")
169
+ ports.append(
170
+ PortMapping(
171
+ int(internal_port),
172
+ int(host_port["HostPort"]),
173
+ protocol,
174
+ )
175
+ )
176
+
177
+ containers.append(
178
+ Container(container.id, container.name, interfaces, ports)
179
+ )
180
+
181
+ return containers, links
182
+
183
+ def draw_network(self, g: Graph, net: Network):
184
+ label = f"{{<gw_iface> {net.gateway} | {net.name}"
185
+ if net.internal:
186
+ label += " | Internal"
187
+ if net.isolated:
188
+ label += " | Containers isolated"
189
+ label += "}"
190
+
191
+ g.node(
192
+ f"network_{net.name}",
193
+ shape="record",
194
+ label=label,
195
+ fillcolor=net.color,
196
+ style="filled",
197
+ )
198
+
199
+ def draw_container(self, g: Graph, c: Container):
200
+ iface_labels = []
201
+ for iface in c.interfaces:
202
+ if iface.aliases:
203
+ iface_labels.append(f"{iface.address} ({', '.join(iface.aliases)})")
204
+ else:
205
+ iface_labels.append(iface.address)
206
+
207
+ port_labels = []
208
+ for port in c.ports:
209
+ port_labels.append(f"{port.internal}->{port.external}/{port.protocol}")
210
+
211
+ label = f"{c.name}\\n"
212
+ label += "Interfaces:\\n" + "\\n".join(iface_labels)
213
+ if port_labels:
214
+ label += "\\nPorts:\\n" + "\\n".join(port_labels)
215
+
216
+ g.node(
217
+ f"container_{c.container_id}",
218
+ shape="box",
219
+ label=label,
220
+ fillcolor="#ff9999",
221
+ style="filled",
222
+ )
223
+
224
+ def draw_link(self, g: Graph, networks: Dict[str, Network], link: Link):
225
+ g.edge(
226
+ f"container_{link.container_id}",
227
+ f"network_{link.network_name}",
228
+ color=networks[link.network_name].color,
229
+ )
230
+
231
+ @method_decorator(csrf_exempt)
232
+ def dispatch(self, *args, **kwargs):
233
+ return super().dispatch(*args, **kwargs)
234
+
235
+ def get_container_stats(self):
236
+ client = docker.from_env()
237
+ stats = []
238
+
239
+ for container in client.containers.list():
240
+ try:
241
+ container_stats = container.stats(stream=False)
242
+
243
+ # Calculate CPU percentage
244
+ cpu_total = float(
245
+ container_stats["cpu_stats"]["cpu_usage"]["total_usage"]
246
+ )
247
+ cpu_delta = cpu_total - float(
248
+ container_stats["precpu_stats"]["cpu_usage"]["total_usage"]
249
+ )
250
+ system_delta = float(
251
+ container_stats["cpu_stats"]["system_cpu_usage"]
252
+ ) - float(container_stats["precpu_stats"]["system_cpu_usage"])
253
+ online_cpus = container_stats["cpu_stats"].get(
254
+ "online_cpus",
255
+ len(
256
+ container_stats["cpu_stats"]["cpu_usage"].get(
257
+ "percpu_usage", [1]
258
+ )
259
+ ),
260
+ )
261
+
262
+ cpu_percent = 0.0
263
+ if system_delta > 0.0:
264
+ cpu_percent = (cpu_delta / system_delta) * 100.0 * online_cpus
265
+
266
+ # Memory usage
267
+ mem_usage = container_stats["memory_stats"].get("usage", 0)
268
+ mem_limit = container_stats["memory_stats"].get("limit", 1)
269
+ mem_percent = (mem_usage / mem_limit) * 100.0
270
+ mem_mb = mem_usage / (1024 * 1024)
271
+
272
+ # Network usage
273
+ net_usage = container_stats.get("networks", {})
274
+ network_in = sum([net.get("rx_bytes", 0) for net in net_usage.values()])
275
+ network_out = sum(
276
+ [net.get("tx_bytes", 0) for net in net_usage.values()]
277
+ )
278
+
279
+ stats.append(
280
+ {
281
+ "name": container.name,
282
+ "cpu_percent": f"{cpu_percent:.2f}%",
283
+ "memory_usage": f"{mem_mb:.2f} MB ({mem_percent:.2f}%)",
284
+ "network_io": f"IN: {network_in/1024:.2f} KB / OUT: {network_out/1024:.2f} KB",
285
+ }
286
+ )
287
+
288
+ except Exception as e:
289
+ stats.append(
290
+ {
291
+ "name": container.name,
292
+ "cpu_percent": "N/A",
293
+ "memory_usage": "N/A",
294
+ "network_io": "N/A",
295
+ }
296
+ )
297
+
298
+ return stats
299
+
300
+ def get(self, request, *args, **kwargs):
301
+ if request.headers.get("X-Requested-With") == "XMLHttpRequest":
302
+ if request.GET.get("type") == "stats":
303
+ return JsonResponse({"stats": self.get_container_stats()})
304
+ elif request.GET.get("type") == "graph":
305
+ return JsonResponse({"graph": self.generate_network_graph()})
306
+
307
+ context = self.get_context_data(**kwargs)
308
+ return self.render_to_response(context)
dStats/wsgi.py ADDED
@@ -0,0 +1,16 @@
1
+ """
2
+ WSGI config for dStats project.
3
+
4
+ It exposes the WSGI callable as a module-level variable named ``application``.
5
+
6
+ For more information on this file, see
7
+ https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
8
+ """
9
+
10
+ import os
11
+
12
+ from django.core.wsgi import get_wsgi_application
13
+
14
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dStats.settings")
15
+
16
+ application = get_wsgi_application()
@@ -0,0 +1,13 @@
1
+ DO WHAT THE F*** YOU WANT TO PUBLIC LICENSE
2
+ Version 2, December 2004
3
+
4
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+ DO WHAT THE F*** YOU WANT TO PUBLIC LICENSE
11
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+ 0. You just DO WHAT THE F*** YOU WANT TO.
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.2
2
+ Name: dStats
3
+ Version: 0.1.0
4
+ Summary: A real-time web-based monitoring tool that provides performance stats for Docker containers and visualizes their network connectivity graph
5
+ Home-page: https://github.com/Arifcse21/dStats
6
+ Author: Abdullah Al Arif
7
+ Author-email: arifcse21@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.12.3
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: Django>=5.1.4
15
+ Requires-Dist: graphviz>=0.20.3
16
+ Requires-Dist: daphne>=4.1.2
17
+ Requires-Dist: requests>=2.32.3
18
+ Requires-Dist: black>=24.10.0
19
+ Dynamic: author
20
+ Dynamic: author-email
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: requires-dist
26
+ Dynamic: requires-python
27
+ Dynamic: summary
28
+
29
+
30
+ # **dStats**
31
+
32
+ **dStats** is a real-time web-based monitoring tool that provides performance stats for Docker containers and visualizes their network connectivity graph.
33
+
34
+ ---
35
+
36
+ ## **Deploy Container Directly**
37
+ Pull and run the container from Docker Hub:
38
+
39
+ ```bash
40
+ docker pull arifcse21/dstats:latest
41
+ ```
42
+
43
+ Run the container:
44
+
45
+ ```bash
46
+ docker run -d --name docker-stats-web --privileged \
47
+ -v /var/run/docker.sock:/var/run/docker.sock \
48
+ -p 2743:2743 arifcse21/dstats:latest
49
+ ```
50
+
51
+ ---
52
+
53
+ ## **Clone the Repository**
54
+
55
+ If you’d like to explore or modify the project, start by cloning the repository:
56
+
57
+ ```bash
58
+ git clone https://github.com/Arifcse21/dStats.git
59
+ cd dStats
60
+ ```
61
+
62
+ ---
63
+
64
+ ## **Run with Docker Manually**
65
+
66
+ Build the Docker image locally:
67
+
68
+ ```bash
69
+ docker build -t dstats:latest .
70
+ ```
71
+
72
+ Run the container:
73
+
74
+ ```bash
75
+ docker run -d --name docker-stats-web --privileged \
76
+ -v /var/run/docker.sock:/var/run/docker.sock \
77
+ -p 2743:2743 dstats:latest
78
+ ```
79
+
80
+ ---
81
+
82
+ ## **Run with Docker Compose**
83
+
84
+ Use Docker Compose for easier setup and teardown:
85
+
86
+ 1. Build and start the services:
87
+
88
+ ```bash
89
+ docker compose up -d
90
+ ```
91
+
92
+ 2. Stop and clean up the services:
93
+
94
+ ```bash
95
+ docker compose down --remove-orphans --rmi all
96
+ ```
97
+
98
+ ---
99
+
100
+ ## **Access the Application**
101
+
102
+ - Open your browser and go to:
103
+ **http://localhost:2743**
104
+
105
+ Here, you’ll find:
106
+ 1. **Container Stats:** Real-time CPU, memory, and network I/O usage.
107
+ 2. **Network Graph:** Visualization of container interconnections.
108
+
109
+ ---
110
+
111
+ ## **Contribute to dStats Project**
112
+
113
+ Thank you for considering contributing to dStats! We appreciate all efforts, big or small, to help improve the project.
114
+
115
+ ### **How to Contribute**
116
+
117
+ We believe collaboration is key to building great software. Here’s how you can get involved:
118
+
119
+ 1. **Report Issues**
120
+ Found a bug? Have a feature request? Open an issue [here](https://github.com/Arifcse21/dStats/issues).
121
+
122
+ 2. **Suggest Enhancements**
123
+ Have an idea for improvement? Share it by opening a discussion or issue.
124
+
125
+ 3. **Contribute Code**
126
+ Whether it’s fixing bugs, adding features, or enhancing documentation, here’s how to start:
127
+ - Fork this repository.
128
+ - Clone your fork:
129
+ ```bash
130
+ git clone https://github.com/your-username/dStats.git
131
+ cd dStats
132
+ ```
133
+ - Create a branch:
134
+ ```bash
135
+ git checkout -b my-feature
136
+ ```
137
+ - Commit your changes:
138
+ ```bash
139
+ git commit -m "Add my feature"
140
+ ```
141
+ - Push your branch:
142
+ ```bash
143
+ git push origin my-feature
144
+ ```
145
+ - Open a pull request on GitHub.
146
+
147
+ 4. **Improve Documentation**
148
+ Good documentation helps everyone. Spot typos? Want to clarify something? Update the `README.md` or other docs and send us a PR.
149
+
150
+ ---
151
+
152
+ ### **Need Help?**
153
+
154
+ Feel free to reach out by opening a discussion on the repository. We’re here to help!
155
+
156
+ Thank you for being part of this project. Together, we can make dStats even better. 🎉
157
+
158
+ ---
@@ -0,0 +1,11 @@
1
+ dStats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ dStats/asgi.py,sha256=MSQjGWX7GgrxE5_GvTsz8PRIiXwgCcx3PIkkUsQ1_tU,389
3
+ dStats/settings.py,sha256=D73YEbwOWk54QosU21YvjSTVJvxkHTqUXiel76Nxpl4,3379
4
+ dStats/urls.py,sha256=R7CHnIif6hO8wr-C_HH7ew3-ZT8KtnE31YO9XT1Q8kU,816
5
+ dStats/views.py,sha256=mjxkwpwwZcS2rAemyaLzfrEOx3WmMHTj3LFGW5ruUwI,9427
6
+ dStats/wsgi.py,sha256=5CVIfhfO5NEUV3gXOmrPPjLNkeararRk6fCzkghNI_Q,389
7
+ dStats-0.1.0.dist-info/LICENSE,sha256=qjNhYHFWgrHF5vENj51WBRO85HtL38pwfTIg8huGj08,483
8
+ dStats-0.1.0.dist-info/METADATA,sha256=dkOcRoxHFYkSXru_0xTWsarzPLLS8XdZIHlr7Qsn8_c,3805
9
+ dStats-0.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
10
+ dStats-0.1.0.dist-info/top_level.txt,sha256=ZqX7qLq-LiHI4j3UAw9S9kHfeD094Jtxc5sdnrcNhU8,7
11
+ dStats-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ dStats