dStats 0.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.
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