├── AUTHORS
├── ChangeLog.creole
├── LICENSE
├── README.creole
├── __init__.py
├── icmp_messages.py
├── ping.py
└── setup.py
/AUTHORS:
--------------------------------------------------------------------------------
1 | AUTHORS / CONTRIBUTORS:
2 |
3 | * Steve Clement -- https://github.com/SteveClement
4 | * FliegendeWurst -- https://github.com/FliegendeWurst
5 | * Cowles, Matthew Dixon -- ftp://ftp.visi.com/users/mdc/ping.py
6 | * Diemer, Jens -- http://www.jensdiemer.de
7 | * Esteban, Mendoza
8 | * Falatic, Martin -- http://www.falatic.com
9 | * Hallman, Chris -- http://cdhallman.blogspot.com
10 | * incidence -- https://github.com/incidence
11 | * jcborras -- https://github.com/jcborras
12 | * Kolev, Georgi -- https://github.com/l4m3rx
13 | * Notaras, George -- http://www.g-loaded.eu
14 | * Poincheval, Jerome
15 | * Sarkhel, Kunal -- https://github.com/techwizrd
16 | * Stauffer, Samuel -- https://github.com/samuel
17 | * Zach Ware
18 | * zed -- https://github.com/zed
19 | * Frassinelli, Francesco -- http://www.frafra.eu
20 | * Estemendoza, Esteban -- https://github.com/estemendoza
21 | * Auke Willem
22 | * Tom Wilkie -- http://hithub.com/tomwilkie
23 | * simudream -- https://github.com/simudream
24 | * Ariana Giroux -- https://github.com/eclectickmedia
25 |
--------------------------------------------------------------------------------
/ChangeLog.creole:
--------------------------------------------------------------------------------
1 | == Revision history ==
2 |
3 | ==== November 4, 2016 ====
4 | ----------------
5 | * Renaming MyStats class to avoid confusion
6 | * Making myStats global variable so we can print stats when terminating with signal
7 | * Fixing small bug in quiet_ping() preventing in from ever exiting.
8 |
9 | ==== November 3, 2016 ====
10 | ----------------
11 | * Converting quiet and verbose ping functions to generator so we can do infinite ping.
12 | All the work was done by Ariana Giroux.
13 | https://github.com/l4m3rx/python-ping/issues/14
14 |
15 | ==== November 3, 2016 ====
16 | ----------------
17 | * Adding exception when sending packets to handle socket.error (Python2.x issue)
18 | Thanks to Ariana Giroux https://github.com/l4m3rx/python-ping/issues/16
19 | * Fixing MyStats printing.
20 |
21 | ==== November 2, 2016 ====
22 | ----------------
23 | * Fix. Replacing print() with print('') to avoid python2.x printing "()" liens
24 | * Introduce a basic CLI interface via Python's argparse by Ariana Giroux
25 | https://github.com/l4m3rx/python-ping/pull/15/commits/0ea79021dfba8ce57ee967f0e923846f627aac71
26 | * Rename variables to avoid overwriting python building functions/objects.
27 | ping.py: bytes -> _bytes
28 | setup.py: hash -> mhash
29 | icmp_messages.py: type -> itype
30 |
31 | ==== November 2, 2016 ====
32 | ----------------
33 | * Updating Authors
34 | * Fixing up function names as suggested by EclectickMedia
35 | https://github.com/l4m3rx/python-ping/issues/13
36 |
37 | ==== October 26, 2016 ====
38 | ----------------
39 | * PEP8 Stuff
40 | * Code clean up
41 | * Remove revision history from ping.py
42 |
43 | ==== October 25, 2016 ====
44 | ----------------
45 | * New unittest (tests.py) thanks to EclectickMedia
46 | * Switching .iteritems() to .items() in icmp_messages.py for better competability (thanks to Steve Clement)
47 | * Small fixes
48 |
49 | ==== October 24, 2016 ====
50 | ----------------
51 | * Python 3 competability
52 | * Fixing small bug (verbose ping printing)
53 |
54 | ==== February 4, 2015 ====
55 | ----------------
56 | * Fix statistics dump on unexpected exit
57 | * Fixing a bug where we match the ICMP request as respons
58 | - Fix based on samuel's work https://github.com/samuel/python-ping/commit/745c159dacce6afebb0f82cac8e9ed5bb2189491#diff-0ee81781c0132dc8f743df3a41b71918R128
59 | * Merging simudream's patch to fix python3 error handeling.
60 |
61 |
62 | ==== January 21, 2015 ====
63 | ----------------
64 | * Set socket options to allow sending pings to broadcast address
65 | - Based on Tom Wilkie's patch
66 | https://github.com/tomwilkie/pyping/commit/7d017b03ee462fae72e2d0ff96a042bcba295564
67 |
68 | ==== August 6, 2014 ====
69 | --------------
70 | * Fixing an bug introduced with last changes :S
71 | - Python on Windows dosn't have socket.inet_ntop
72 | Adding try/except to handle AttributeError.
73 |
74 | ==== June 30, 2014 ====
75 | -------------
76 | * For problems/suggestions https://github.com/l4m3rx/python-ping/issues
77 |
78 | * Forgot to add stuff to this README :p
79 |
80 | ==== June 29, 2014 ====
81 | -------------
82 | * Merging parts of code from all the python-ping forks on github
83 | * Litlle modifications by Auke Willem Oosterhoff:
84 | - Added support for simultaneous pings on multiple hosts.
85 | https://bitbucket.org/OrangeTux/python-ping/commits/d4aa720662995a57cf18fa6b8ea689e9d11d26c7/raw/
86 |
87 | * Faster and cleaner checksum creation
88 | Based on frfra's patch
89 | https://github.com/alexlouden/python-ping/commit/b9fc3acb2c36ccc895d1f7ba7336b951dc033ce9
90 |
91 | * Changing 'except' calls to work with 2.x and 3.x
92 | Based on pferate's patch
93 | https://github.com/pferate/python_ping/commit/4e761ea99582ac1699c7965d149ce16e6b62f0ac
94 |
95 | * Some small PIP8 stuff...
96 |
97 | * Merging stuff from/with https://github.com/estemendoza/python-ping
98 | Both repos are in sync now.
99 |
100 | ==== June 19, 2013 ====
101 | --------------
102 | * Added support for IPv6. Taken from implementation of Lars Strand.
103 |
104 | ==== March 19, 2013 ====
105 | --------------
106 | * Fixing bug to prevent divide by 0 during run-time.
107 |
108 | ==== January 26, 2012 ====
109 | ----------------
110 | * Fixing BUG #4 - competability with python 2.x [tested with 2.7]
111 | - Packet data building is different for 2.x and 3.x.
112 | 'cose of the string/bytes difference.
113 | * Fixing BUG #10 - the multiple resolv issue.
114 | - When pinging domain names insted of hosts (for exmaple google.com)
115 | you can get different IP every time you try to resolv it, we should
116 | resolv the host only once and stick to that IP.
117 | * Fixing BUGs #3 #10 - Doing hostname resolv only once.
118 | * Fixing BUG #14 - Removing all 'global' stuff.
119 | - You should not use globul! Its bad for you...and its not thread safe!
120 | * Fix - forcing the use of different times on linux/windows for
121 | more accurate mesurments. (time.time - linux/ time.clock - windows)
122 | * Adding quiet_ping function - This way we'll be able to use this script
123 | as external lib.
124 | * Changing default timeout to 3s. (1second is not enought)
125 | * Switching data syze to packet size. It's easyer for the user to ignore the
126 | fact that the packet headr is 8b and the datasize 64 will make packet with
127 |
128 | ==== Oct. 17, 2011 ====
129 | --------------
130 | * [[https://github.com/jedie/python-ping/pull/6|Bugfix if host is unknown]]
131 |
132 | ==== Oct. 12, 2011 ====
133 | --------------
134 | Merge sources and create a seperate github repository:
135 | * https://github.com/jedie/python-ping
136 |
137 | Add a simple CLI interface.
138 |
139 | ==== September 12, 2011 ====
140 | --------------
141 | * Bugfixes + cleanup by Jens Diemer
142 | Tested with Ubuntu + Windows 7
143 |
144 | ==== September 6, 2011 ====
145 | --------------
146 | * [[http://www.falatic.com/index.php/39/pinging-with-python|Cleanup by Martin Falatic.]]
147 | Restored lost comments and docs. Improved functionality: constant time between
148 | pings, internal times consistently use milliseconds. Clarified annotations
149 | (e.g., in the checksum routine). Using unsigned data in IP & ICMP header
150 | pack/unpack unless otherwise necessary. Signal handling. Ping-style output
151 | formatting and stats.
152 |
153 | ==== August 3, 2011 ====
154 | --------------
155 | * Ported to py3k by Zach Ware. Mostly done by 2to3; also minor changes to
156 | deal with bytes vs. string changes (no more ord() in checksum() because
157 | >source_string< is actually bytes, added .encode() to data in
158 | send_one_ping()). That's about it.
159 |
160 | ==== March 11, 2010 ====
161 | --------------
162 | * changes by Samuel Stauffer:
163 | replaced time.clock with default_timer which is set to
164 | time.clock on windows and time.time on other systems.
165 |
166 | ==== November 8, 2009 ====
167 | --------------
168 | * Fixes by [[http://www.g-loaded.eu/2009/10/30/python-ping/|George Notaras]],
169 | reported by [[http://cdhallman.blogspot.com|Chris Hallman]]:
170 |
171 | * Improved compatibility with GNU/Linux systems.
172 |
173 | * Changes in this release:
174 | Re-use time.time() instead of time.clock(). The 2007 implementation
175 | worked only under Microsoft Windows. Failed on GNU/Linux.
176 | time.clock() behaves differently under [[http://docs.python.org/library/time.html#time.clock|the two OSes]].
177 |
178 |
179 | ==== May 30, 2007 ====
180 | --------------
181 | little [[http://www.python-forum.de/post-69122.html#69122|rewrite by Jens Diemer]]:
182 | * change socket asterisk import to a normal import
183 | * replace time.time() with time.clock()
184 | * delete "return None" (or change to "return" only)
185 | * in checksum() rename "str" to "source_string"
186 |
187 | ==== December 4, 2000 ====
188 | --------------
189 | * Changed the struct.pack() calls to pack the checksum and ID as
190 | unsigned. My thanks to Jerome Poincheval for the fix.
191 |
192 | ==== November 22, 1997 ====
193 | --------------
194 | * Initial hack. Doesn't do much, but rather than try to guess
195 | what features I (or others) will want in the future, I've only
196 | put in what I need now.
197 |
198 | ==== December 16, 1997 ====
199 | For some reason, the checksum bytes are in the wrong order when
200 | this is run under Solaris 2.X for SPARC but it works right under
201 | Linux x86. Since I don't know just what's wrong, I'll swap the
202 | bytes always and then do an htons().
203 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | A pure python implementation of netkit's ping.c
2 |
3 | Copyright (C) 2016, python-ping team
4 |
5 | This program is free software; you can redistribute it and/or
6 | modify it under the terms of the GNU General Public License
7 | as published by the Free Software Foundation; either version 2
8 | of the License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU General Public License for more details.
14 |
15 | You should have received a copy of the GNU General Public License
16 | along with this program; if not, write to the Free Software
17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 |
19 | To report a change or a bug, open an issue on
20 | https://github.com/l4m3rx/python-ping or email one of the contributors.
21 |
22 | -------------------------------------------------------------------------------
23 | PREVIOUS LICENSING STATEMENT.
24 |
25 | The original code derived from ping.c distributed in Linux's netkit.
26 | That code is copyright (c) 1989 by The Regents of the University of California.
27 | That code is in turn derived from code written by Mike Muuss of the
28 | US Army Ballistic Research Laboratory in December, 1983 and
29 | placed in the public domain. They have my thanks.
30 |
31 | Copyright (c) Matthew Dixon Cowles, .
32 | Distributable under the terms of the GNU General Public License
33 | version 2. Provided with no warranties of any sort.
34 |
35 | See AUTHORS for complete list of authors and contributors.
36 |
--------------------------------------------------------------------------------
/README.creole:
--------------------------------------------------------------------------------
1 | == python-ping ==
2 | A pure python ping implementation using raw sockets.
3 | * Compatible with Python2 & Python3
4 | * Note that ICMP messages can only be sent from processes running as root
5 | (in Windows, you must run this script as 'Administrator').
6 |
7 | == Original Version ==
8 | * [[ftp://ftp.visi.com/users/mdc/ping.py|Matthew Dixon Cowles]]
9 | * copyleft 1989-2016 by the python-ping team, see [[https://github.com/l4m3rx/python-ping/blob/master/AUTHORS|AUTHORS]] for more details.
10 | * license: GNU GPL v2, see [[https://github.com/l4m3rx/python-ping/blob/master/LICENSE|LICENSE]] for more details.
11 |
12 | === Usage ===
13 |
14 | {{{
15 | $ sudo python3 ping.py -h
16 | usage: python-ping [-h] [-t TIMEOUT] [-c REQUEST_COUNT] [-i] [-I]
17 | [-s PACKET_SIZE] [-T]
18 | address
19 |
20 | A pure python implementation of the ping protocol. *REQUIRES ROOT*
21 |
22 | positional arguments:
23 | address The address to attempt to ping.
24 |
25 | optional arguments:
26 | -h, --help show this help message and exit
27 | -t TIMEOUT, --timeout TIMEOUT
28 | The maximum amount of time to wait until ping timeout.
29 | -c REQUEST_COUNT, --request_count REQUEST_COUNT
30 | The number of attempts to make. See --infinite to
31 | attempt requests until stopped.
32 | -i, --infinite Flag to continuously ping a host until stopped.
33 | -I, --ipv6 Flag to use IPv6.
34 | -s PACKET_SIZE, --packet_size PACKET_SIZE
35 | Designate the amount of data to send per packet.
36 | -T, --test_case Flag to run the default test case suite.
37 | }}}
38 |
39 |
40 | === Using as lib ===
41 | {{{
42 | Python 2.7.11 (default, Mar 3 2016, 13:35:30)
43 | [GCC 5.3.0] on linux2
44 | Type "help", "copyright", "credits" or "license" for more information.
45 | >>> import ping
46 | >>> ping.verbose_ping('google.com', timeout=3000, count=3, numDataBytes=1300)
47 |
48 | >>> list(ping.verbose_ping('google.com', timeout=3000, count=3, numDataBytes=1300))
49 |
50 | PYTHON PING google.com (216.58.212.46): 1300 data bytes
51 | 72 bytes from 216.58.212.46: icmp_seq=0 ttl=59 time=4.42 ms
52 | 72 bytes from 216.58.212.46: icmp_seq=1 ttl=59 time=4.70 ms
53 | 72 bytes from 216.58.212.46: icmp_seq=2 ttl=59 time=4.44 ms
54 | 72 bytes from 216.58.212.46: icmp_seq=3 ttl=59 time=4.47 ms
55 |
56 | ----216.58.212.46 PYTHON PING Statistics----
57 | 4 packets transmitted, 4 packets received, 0.0% packet loss
58 | round-trip (ms) min/avg/max = 4.4/4.5/4.7
59 |
60 | [1, 2, 3, False]
61 |
62 |
63 | >>> list(ping.quiet_ping('google.com', timeout=3000, count=3, numDataBytes=1300))
64 | [1, 2, 3, (4.508256912231445, 4.332065582275391, 4.399617513020833, 0.0)]
65 | >>> list(ping.quiet_ping('google.com', timeout=3000, count=3, numDataBytes=1300))[-1]
66 | (4.535675048828125, 4.359245300292969, 4.423936208089192, 0.0)
67 |
68 | }}}
69 |
70 | == TODOs ==
71 | * Make delay between sending packets as input parm.
72 |
73 | == contribute ==
74 |
75 | [[http://help.github.com/fork-a-repo/|Fork this repo]] on [[https://github.com/l4m3rx/python-ping/|GitHub]] and [[http://help.github.com/send-pull-requests/|send pull requests]]. Thank you.
76 |
77 |
78 | == Revision history ==
79 | [[https://github.com/l4m3rx/python-ping/blob/master/ChangeLog.creole|ChangeLog / Revision history]]
80 |
81 |
82 | == Links ==
83 |
84 | | Sourcecode at GitHub | https://github.com/l4m3rx/python-ping |
85 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/l4m3rx/python-ping/64d75011ab129150b8f6dd6f96dd70a969b09243/__init__.py
--------------------------------------------------------------------------------
/icmp_messages.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | ICMP Control Messages
6 | https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol#Control_messages
7 | ICMP Types 0, 3, 4, 5, 8, 11, 12, 13, 14 from RFC792
8 | ICMP Types 9, 11 from RFC1256
9 | """
10 | ICMP_CONTROL_MESSAGE = \
11 | {0: {0: 'Echo reply',
12 | },
13 | 3: {0: 'Destination network unreachable',
14 | 1: 'Destination host unreachable',
15 | 2: 'Destination protocol unreachable',
16 | 3: 'Destination port unreachable',
17 | 4: 'Fragmentation required, and DF flag set',
18 | 5: 'Source route failed',
19 | 6: 'Destination network unknown',
20 | 7: 'Destination host unknown',
21 | 8: 'Source host isolated',
22 | 9: 'Network administratively prohibited',
23 | 10: 'Host administratively prohibited',
24 | 11: 'Network unreachable for TOS',
25 | 12: 'Host unreachable for TOS',
26 | 13: 'Communication administratively prohibited',
27 | 14: 'Host Precedence Violation',
28 | 15: 'Precedence cutoff in effect',
29 | },
30 | 4: {0: 'Source quench',
31 | },
32 | 5: {0: 'Redirect Datagram for the Network',
33 | 1: 'Redirect Datagram for the Host',
34 | 2: 'Redirect Datagram for the TOS & network',
35 | 3: 'Redirect Datagram for the TOS & host',
36 | },
37 | 8: {0: 'Echo request',
38 | },
39 | 9: {0: 'Router Advertisement',
40 | },
41 | 10:{0: 'Router discovery/selection/solicitation',
42 | },
43 | 11:{0: 'TTL expired in transit',
44 | 1: 'Fragment reassembly time exceeded',
45 | },
46 | 12:{0: 'Pointer indicates the error',
47 | 1: 'Missing a required option',
48 | 2: 'Bad length',
49 | },
50 | 13:{0: 'Timestamp',
51 | },
52 | 14:{0: 'Timestamp reply',
53 | },
54 | }
55 |
56 |
57 | """
58 | ICMPv6 Control Messages
59 | https://en.wikipedia.org/wiki/ICMPv6#Types_of_ICMPv6_messages
60 | ICMPv6 Types 0-127 are Error Messages
61 | ICMPv6 Types 128-255 are Informational Messages
62 | """
63 | ICMPv6_CONTROL_MESSAGE = \
64 | {1: {0: 'no route to destination',
65 | 1: 'communication with destination administratively prohibited',
66 | 2: 'beyond scope of source address',
67 | 3: 'address unreachable',
68 | 4: 'port unreachable',
69 | 5: 'source address failed ingress/egress policy',
70 | 6: 'reject route to destination',
71 | 7: 'Error in Source Routing Header',
72 | },
73 | 2: {0: 'packet too big',
74 | },
75 | 3: {0: 'hop limit exceeded in transit',
76 | 1: 'fragment reassembly time exceeded',
77 | },
78 | 4: {0: 'erroneous header field encountered',
79 | 1: 'unrecognized Next Header type encountered',
80 | 2: 'unrecognized IPv6 option encountered',
81 | },
82 | }
83 |
84 | if __name__ == '__main__':
85 | # Print all defined ICMP Control Messages
86 | print("ICMP Control Messages")
87 | print("Type\tCode:\tMessage")
88 | for (itype, codes) in ICMP_CONTROL_MESSAGE.items():
89 | print("")
90 | for (code, message) in codes.items():
91 | print("[%d]\t[%d]:\t%s" % (itype, code, message))
92 | print("")
93 |
94 | # Print all defined ICMPv6 Control Messages
95 | print("ICMPv6 Control Messages")
96 | print("Type\tCode:\tMessage")
97 | for (itype, codes) in ICMPv6_CONTROL_MESSAGE.items():
98 | print("")
99 | for (code, message) in codes.items():
100 | print("[%d]\t[%d]:\t%s" % (itype, code, message))
101 |
--------------------------------------------------------------------------------
/ping.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | from __future__ import division, print_function
5 |
6 | """
7 | A pure python ping implementation using raw sockets.
8 |
9 | Compatibility:
10 | OS: Linux, Windows, MacOSX
11 | Python: 2.6 - 3.5
12 |
13 | Note that due to the usage of RAW sockets root/Administrator
14 | privileges are requied.
15 |
16 | Derived from ping.c distributed in Linux's netkit. That code is
17 | copyright (c) 1989 by The Regents of the University of California.
18 | That code is in turn derived from code written by Mike Muuss of the
19 | US Army Ballistic Research Laboratory in December, 1983 and
20 | placed in the public domain. They have my thanks.
21 |
22 | Copyright (c) Matthew Dixon Cowles, .
23 | Distributable under the terms of the GNU General Public License
24 | version 2. Provided with no warranties of any sort.
25 |
26 | website: https://github.com/l4m3rx/python-ping
27 |
28 | """
29 |
30 | # TODO Remove any calls to time.sleep
31 | # This would enable extension into larger framework that aren't multi threaded.
32 | import os
33 | import sys
34 | import time
35 | import array
36 | import socket
37 | import struct
38 | import select
39 | import signal
40 |
41 | if __name__ == '__main__':
42 | import argparse
43 |
44 |
45 | try:
46 | from _thread import get_ident
47 | except ImportError:
48 | def get_ident(): return 0
49 |
50 | if sys.platform == "win32":
51 | # On Windows, the best timer is time.clock()
52 | default_timer = time.clock
53 | else:
54 | # On most other platforms the best timer is time.time()
55 | default_timer = time.time
56 |
57 | # ICMP parameters
58 |
59 | ICMP_ECHOREPLY = 0 # Echo reply (per RFC792)
60 | ICMP_ECHO = 8 # Echo request (per RFC792)
61 | ICMP_ECHO_IPV6 = 128 # Echo request (per RFC4443)
62 | ICMP_ECHO_IPV6_REPLY = 129 # Echo request (per RFC4443)
63 | ICMP_MAX_RECV = 2048 # Max size of incoming buffer
64 |
65 | MAX_SLEEP = 1000
66 |
67 |
68 | class MStats2(object):
69 |
70 | def __init__(self):
71 | self._this_ip = '0.0.0.0'
72 | self.reset()
73 |
74 | def reset(self):
75 | self._timing_list = []
76 | self._packets_sent = 0
77 | self._packets_rcvd = 0
78 |
79 | self._reset_statistics()
80 |
81 | @property
82 | def thisIP(self):
83 | return self._this_ip
84 |
85 | @thisIP.setter
86 | def thisIP(self, value):
87 | self._this_ip = value
88 |
89 | @property
90 | def pktsSent(self):
91 | return self._packets_sent
92 |
93 | @property
94 | def pktsRcvd(self):
95 | return self._packets_rcvd
96 |
97 | @property
98 | def pktsLost(self):
99 | return self._packets_sent - self._packets_rcvd
100 |
101 | @property
102 | def minTime(self):
103 | return min(self._timing_list) if self._timing_list else None
104 |
105 | @property
106 | def maxTime(self):
107 | return max(self._timing_list) if self._timing_list else None
108 |
109 | @property
110 | def totTime(self):
111 | if self._total_time is None:
112 | self._total_time = sum(self._timing_list)
113 | return self._total_time
114 |
115 | def _get_mean_time(self):
116 | if self._mean_time is None:
117 | if len(self._timing_list) > 0:
118 | self._mean_time = self.totTime / len(self._timing_list)
119 | return self._mean_time
120 | mean_time = property(_get_mean_time)
121 | avrgTime = property(_get_mean_time)
122 |
123 | @property
124 | def median_time(self):
125 | if self._median_time is None:
126 | self._median_time = self._calc_median_time()
127 | return self._median_time
128 |
129 | @property
130 | def pstdev_time(self):
131 | """Returns the 'Population Standard Deviation' of the set."""
132 | if self._pstdev_time is None:
133 | self._pstdev_time = self._calc_pstdev_time()
134 | return self._pstdev_time
135 |
136 | @property
137 | def fracLoss(self):
138 | if self._frac_loss is None:
139 | if self.pktsSent > 0:
140 | self._frac_loss = self.pktsLost / self.pktsSent
141 | return self._frac_loss
142 |
143 | def packet_sent(self, n=1):
144 | self._packets_sent += n
145 |
146 | def packet_received(self, n=1):
147 | self._packets_rcvd += n
148 |
149 | def record_time(self, value):
150 | self._timing_list.append(value)
151 | self._reset_statistics()
152 |
153 | def _reset_statistics(self):
154 | self._total_time = None
155 | self._mean_time = None
156 | self._median_time = None
157 | self._pstdev_time = None
158 | self._frac_loss = None
159 |
160 | def _calc_median_time(self):
161 | n = len(self._timing_list)
162 | if n == 0:
163 | return None
164 | if n & 1 == 1: # Odd number of samples? Return the middle.
165 | return sorted(self._timing_list)[n//2]
166 | else: # Even number of samples? Return the mean of the two middle samples.
167 | halfn = n // 2
168 | return sum(sorted(self._timing_list)[halfn:halfn+2]) / 2
169 |
170 | def _calc_sum_square_time(self):
171 | mean = self.mean_time
172 | return sum(((t - mean)**2 for t in self._timing_list))
173 |
174 | def _calc_pstdev_time(self):
175 | pvar = self._calc_sum_square_time() / len(self._timing_list)
176 | return pvar**0.5
177 |
178 |
179 | # Used as 'global' variale so we can print
180 | # stats when exiting by signal
181 | myStats = MStats2()
182 |
183 |
184 | def _checksum(source_string):
185 | """
186 | A port of the functionality of in_cksum() from ping.c
187 | Ideally this would act on the string as a series of 16-bit ints (host
188 | packed), but this works.
189 | Network data is big-endian, hosts are typically little-endian
190 | """
191 | if (len(source_string) % 2):
192 | source_string += "\x00"
193 | converted = array.array("H", source_string)
194 | if sys.byteorder == "big":
195 | converted.bytewap()
196 | val = sum(converted)
197 |
198 | val &= 0xffffffff # Truncate val to 32 bits (a variance from ping.c, which
199 | # uses signed ints, but overflow is unlikely in ping)
200 |
201 | val = (val >> 16) + (val & 0xffff) # Add high 16 bits to low 16 bits
202 | val += (val >> 16) # Add carry from above (if any)
203 | answer = ~val & 0xffff # Invert and truncate to 16 bits
204 | answer = socket.htons(answer)
205 |
206 | return answer
207 |
208 |
209 | def single_ping(destIP, hostname, timeout, mySeqNumber, numDataBytes,
210 | myStats=None, ipv6=False, verbose=True, sourceIP=None):
211 | """
212 | Returns either the delay (in ms) or None on timeout.
213 | """
214 | delay = None
215 |
216 | if ipv6:
217 | try: # One could use UDP here, but it's obscure
218 | mySocket = socket.socket(socket.AF_INET6, socket.SOCK_RAW,
219 | socket.getprotobyname("ipv6-icmp"))
220 | if sourceIP is not None:
221 | mySocket.bind((sourceIP, 0))
222 | mySocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
223 | except OSError as e:
224 | if verbose:
225 | print("failed. (socket error: '%s')" % str(e))
226 | print('Note that python-ping uses RAW sockets'
227 | 'and requiers root rights.')
228 | raise # raise the original error
229 | else:
230 |
231 | try: # One could use UDP here, but it's obscure
232 | mySocket = socket.socket(socket.AF_INET, socket.SOCK_RAW,
233 | socket.getprotobyname("icmp"))
234 | if sourceIP is not None:
235 | mySocket.bind((sourceIP, 0))
236 | mySocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
237 | except OSError as e:
238 | if verbose:
239 | print("failed. (socket error: '%s')" % str(e))
240 | print('Note that python-ping uses RAW sockets'
241 | 'and requires root rights.')
242 | raise # raise the original error
243 |
244 | my_ID = (os.getpid() ^ get_ident()) & 0xFFFF
245 |
246 | sentTime = _send(mySocket, destIP, my_ID, mySeqNumber, numDataBytes, ipv6,
247 | verbose)
248 | if sentTime is None:
249 | mySocket.close()
250 | return delay, (None,)
251 |
252 | if myStats is not None:
253 | myStats.packet_sent()
254 |
255 | recvTime, dataSize, iphSrcIP, icmpSeqNumber, iphTTL \
256 | = _receive(mySocket, my_ID, timeout, ipv6)
257 |
258 | mySocket.close()
259 |
260 | if recvTime:
261 | delay = (recvTime-sentTime)*1000
262 | if ipv6:
263 | host_addr = hostname
264 | else:
265 | try:
266 | host_addr = socket.inet_ntop(socket.AF_INET, struct.pack(
267 | "!I", iphSrcIP))
268 | except AttributeError:
269 | # Python on windows dosn't have inet_ntop.
270 | host_addr = hostname
271 |
272 | if verbose:
273 | print("%d bytes from %s: icmp_seq=%d ttl=%d time=%.2f ms" % (
274 | dataSize, host_addr, icmpSeqNumber, iphTTL, delay)
275 | )
276 |
277 | if myStats is not None:
278 | assert isinstance(myStats, MStats2)
279 | myStats.packet_received()
280 | myStats.record_time(delay)
281 | else:
282 | delay = None
283 | if verbose:
284 | print("Request timed out.")
285 |
286 | return delay, (recvTime, dataSize, iphSrcIP, icmpSeqNumber, iphTTL)
287 |
288 |
289 | def _send(mySocket, destIP, myID, mySeqNumber, numDataBytes, ipv6=False,
290 | verbose=True):
291 | """
292 | Send one ping to the given >destIP<.
293 | """
294 | # destIP = socket.gethostbyname(destIP)
295 |
296 | # Header is type (8), code (8), checksum (16), id (16), sequence (16)
297 | # (numDataBytes - 8) - Remove header size from packet size
298 | myChecksum = 0
299 |
300 | # Make a dummy heder with a 0 checksum.
301 | if ipv6:
302 | header = struct.pack(
303 | "!BbHHh", ICMP_ECHO_IPV6, 0, myChecksum, myID, mySeqNumber
304 | )
305 | else:
306 | header = struct.pack(
307 | "!BBHHH", ICMP_ECHO, 0, myChecksum, myID, mySeqNumber
308 | )
309 |
310 | padBytes = []
311 | startVal = 0x42
312 | # 'cose of the string/byte changes in python 2/3 we have
313 | # to build the data differnely for different version
314 | # or it will make packets with unexpected size.
315 | if sys.version[:1] == '2':
316 | _bytes = struct.calcsize("d")
317 | data = ((numDataBytes - 8) - _bytes) * "Q"
318 | data = struct.pack("d", default_timer()) + data
319 | else:
320 | for i in range(startVal, startVal + (numDataBytes - 8)):
321 | padBytes += [(i & 0xff)] # Keep chars in the 0-255 range
322 | # data = bytes(padBytes)
323 | data = bytearray(padBytes)
324 |
325 | # Calculate the checksum on the data and the dummy header.
326 | myChecksum = _checksum(header + data) # Checksum is in network order
327 |
328 | # Now that we have the right checksum, we put that in. It's just easier
329 | # to make up a new header than to stuff it into the dummy.
330 | if ipv6:
331 | header = struct.pack(
332 | "!BbHHh", ICMP_ECHO_IPV6, 0, myChecksum, myID, mySeqNumber
333 | )
334 | else:
335 | header = struct.pack(
336 | "!BBHHH", ICMP_ECHO, 0, myChecksum, myID, mySeqNumber
337 | )
338 |
339 | packet = header + data
340 |
341 | sendTime = default_timer()
342 |
343 | try:
344 | mySocket.sendto(packet, (destIP, 1)) # Port number is irrelevant
345 | except OSError as e:
346 | if verbose:
347 | print("General failure (%s)" % str(e))
348 | return
349 | except socket.error as e:
350 | if verbose:
351 | print("General failure (%s)" % str(e))
352 | return
353 |
354 | return sendTime
355 |
356 |
357 | def _receive(mySocket, myID, timeout, ipv6=False):
358 | """
359 | Receive the ping from the socket. Timeout = in ms
360 | """
361 | timeLeft = timeout/1000
362 |
363 | while True: # Loop while waiting for packet or timeout
364 | startedSelect = default_timer()
365 | whatReady = select.select([mySocket], [], [], timeLeft)
366 | howLongInSelect = (default_timer() - startedSelect)
367 | if whatReady[0] == []: # Timeout
368 | return None, 0, 0, 0, 0
369 |
370 | timeReceived = default_timer()
371 |
372 | recPacket, addr = mySocket.recvfrom(ICMP_MAX_RECV)
373 |
374 | ipHeader = recPacket[:20]
375 |
376 | iphVersion, iphTypeOfSvc, iphLength, iphID, iphFlags, iphTTL, \
377 | iphProtocol, iphChecksum, iphSrcIP, iphDestIP = struct.unpack(
378 | "!BBHHHBBHII", ipHeader)
379 |
380 | if ipv6:
381 | icmpHeader = recPacket[0:8]
382 | else:
383 | icmpHeader = recPacket[20:28]
384 |
385 | icmpType, icmpCode, icmpChecksum, icmpPacketID, icmpSeqNumber \
386 | = struct.unpack("!BBHHH", icmpHeader)
387 |
388 | # Match only the packets we care about
389 | if (icmpType != 8) and (icmpPacketID == myID):
390 | dataSize = len(recPacket) - 28
391 | return timeReceived, (dataSize + 8), iphSrcIP, icmpSeqNumber, \
392 | iphTTL
393 |
394 | timeLeft = timeLeft - howLongInSelect
395 | if timeLeft <= 0:
396 | return None, 0, 0, 0, 0
397 |
398 |
399 | def _dump_stats(myStats):
400 | """
401 | Show stats when pings are done
402 | """
403 | print("\n----%s PYTHON PING Statistics----" % (myStats.thisIP))
404 |
405 | print("%d packets transmitted, %d packets received, %0.1f%% packet loss"
406 | % (myStats.pktsSent, myStats.pktsRcvd, 100.0 * myStats.fracLoss))
407 |
408 | if myStats.pktsRcvd > 0:
409 | print("round-trip (ms) min/avg/max = %0.1f/%0.1f/%0.1f" % (
410 | myStats.minTime, myStats.avrgTime, myStats.maxTime
411 | ))
412 | print(' median/pstddev = %0.2f/%0.2f' % (
413 | myStats.median_time, myStats.pstdev_time
414 | ))
415 |
416 | print('')
417 | return
418 |
419 |
420 | def _signal_handler(signum, frame):
421 | """ Handle exit via signals """
422 | global myStats
423 | _dump_stats(myStats)
424 | print("\n(Terminated with signal %d)\n" % (signum))
425 | sys.exit(0)
426 |
427 |
428 | def _pathfind_ping(destIP, hostname, timeout, mySeqNumber, numDataBytes,
429 | ipv6=None, sourceIP=None):
430 | single_ping(destIP, hostname, timeout,
431 | mySeqNumber, numDataBytes, ipv6=ipv6, verbose=False, sourceIP=sourceIP)
432 | time.sleep(0.5)
433 |
434 |
435 | def verbose_ping(hostname, timeout=3000, count=3,
436 | numDataBytes=64, path_finder=False, ipv6=False, sourceIP=None):
437 | """
438 | Send >count< ping to >destIP< with the given >timeout< and display
439 | the result.
440 |
441 | To continuously attempt ping requests, set >count< to None.
442 |
443 | To consume the generator, use the following syntax:
444 | >>> import ping
445 | >>> for return_val in ping.verbose_ping('google.ca'):
446 | pass # COLLECT YIELDS AND PERFORM LOGIC.
447 |
448 | Alternatively, you can consume the generator by using list comprehension:
449 | >>> import ping
450 | >>> consume = list(ping.verbose_ping('google.ca'))
451 |
452 | Via the same syntax, you can successfully get the exit code via:
453 | >>> import ping
454 | >>> consume = list(ping.verbose_ping('google.ca'))
455 | >>> exit_code = consume[:-1] # The last yield is the exit code.
456 | >>> sys.exit(exit_code)
457 | """
458 |
459 | global myStats
460 |
461 | # Handle Ctrl+C
462 | signal.signal(signal.SIGINT, _signal_handler)
463 | if hasattr(signal, "SIGBREAK"): # Handle Ctrl-Break /Windows/
464 | signal.signal(signal.SIGBREAK, _signal_handler)
465 |
466 | myStats = MStats2() # Reset the stats
467 | mySeqNumber = 0 # Starting value
468 |
469 | try:
470 | if ipv6:
471 | info = socket.getaddrinfo(hostname, None)[0]
472 | destIP = info[4][0]
473 | else:
474 | destIP = socket.gethostbyname(hostname)
475 | print("\nPYTHON PING %s (%s): %d data bytes" % (hostname, destIP,
476 | numDataBytes))
477 | except socket.gaierror as e:
478 | print("\nPYTHON PING: Unknown host: %s (%s)" % (hostname, str(e)))
479 | print('')
480 | return
481 |
482 | myStats.thisIP = destIP
483 |
484 | # This will send packet that we don't care about 0.5 seconds before it
485 | # starts actually pinging. This is needed in big MAN/LAN networks where
486 | # you sometimes loose the first packet. (while the switches find the way)
487 | if path_finder:
488 | print("PYTHON PING %s (%s): Sending pathfinder ping" % (hostname, destIP))
489 | _pathfind_ping(destIP, hostname, timeout,
490 | mySeqNumber, numDataBytes, ipv6=ipv6, sourceIP=sourceIP)
491 | print()
492 |
493 | i = 0
494 | while 1:
495 | delay = single_ping(destIP, hostname, timeout, mySeqNumber,
496 | numDataBytes, ipv6=ipv6, myStats=myStats, sourceIP=sourceIP)
497 | delay = 0 if delay is None else delay[0]
498 |
499 | mySeqNumber += 1
500 |
501 | # Pause for the remainder of the MAX_SLEEP period (if applicable)
502 | if (MAX_SLEEP > delay):
503 | time.sleep((MAX_SLEEP - delay)/1000)
504 |
505 | if count is not None and i < count:
506 | i += 1
507 | yield myStats.pktsRcvd
508 | elif count is None:
509 | yield myStats.pktsRcvd
510 | elif count is not None and i >= count:
511 | break
512 |
513 | _dump_stats(myStats)
514 | # 0 if we receive at least one packet
515 | # 1 if we don't receive any packets
516 | yield not myStats.pktsRcvd
517 |
518 |
519 | def quiet_ping(hostname, timeout=3000, count=3, advanced_statistics=False,
520 | numDataBytes=64, path_finder=False, ipv6=False, sourceIP=None):
521 | """ Same as verbose_ping, but the results are yielded as a tuple """
522 | myStats = MStats2() # Reset the stats
523 | mySeqNumber = 0 # Starting value
524 |
525 | try:
526 | if ipv6:
527 | info = socket.getaddrinfo(hostname, None)[0]
528 | destIP = info[4][0]
529 | else:
530 | destIP = socket.gethostbyname(hostname)
531 | except socket.gaierror:
532 | yield False
533 | return
534 |
535 | myStats.thisIP = destIP
536 |
537 | # This will send packet that we don't care about 0.5 seconds before it
538 | # starts actually pinging. This is needed in big MAN/LAN networks where
539 | # you sometimes loose the first packet. (while the switches find the way)
540 | if path_finder:
541 | _pathfind_ping(destIP, hostname, timeout,
542 | mySeqNumber, numDataBytes, ipv6=ipv6, sourceIP=sourceIP)
543 |
544 | i = 1
545 | while 1:
546 | delay = single_ping(destIP, hostname, timeout, mySeqNumber,
547 | numDataBytes, ipv6=ipv6, myStats=myStats,
548 | verbose=False, sourceIP=sourceIP)
549 | delay = 0 if delay is None else delay[0]
550 |
551 | mySeqNumber += 1
552 | # Pause for the remainder of the MAX_SLEEP period (if applicable)
553 | if (MAX_SLEEP > delay):
554 | time.sleep((MAX_SLEEP - delay) / 1000)
555 |
556 | yield myStats.pktsSent
557 | if count is not None and i < count:
558 | i += 1
559 | elif count is not None and i >= count:
560 | break
561 | elif count is not None:
562 | yield myStats.pktsSent
563 |
564 | if advanced_statistics:
565 | # return tuple(max_rtt, min_rtt, avrg_rtt, percent_lost, median, pop.std.dev)
566 | yield myStats.maxTime, myStats.minTime, myStats.avrgTime, myStats.fracLoss,\
567 | myStats.median_time, myStats.pstdev_time
568 | else:
569 | # return tuple(max_rtt, min_rtt, avrg_rtt, percent_lost)
570 | yield myStats.maxTime, myStats.minTime, myStats.avrgTime, myStats.fracLoss
571 |
572 |
573 | if __name__ == '__main__':
574 | # FIXME: Add a real CLI (mostly fixed)
575 | if sys.argv.count('-T') or sys.argv.count('--test_case'):
576 | print('Running PYTHON PING test case.')
577 | # These should work:
578 | for val in verbose_ping("127.0.0.1"):
579 | pass
580 | for val in verbose_ping("8.8.8.8"):
581 | pass
582 | for val in verbose_ping("heise.de"):
583 | pass
584 | for val in verbose_ping("google.com"):
585 | pass
586 |
587 | # Inconsistent on Windows w/ ActivePython (Python 3.2 resolves
588 | # correctly to the local host, but 2.7 tries to resolve to the local
589 | # *gateway*)
590 | for val in verbose_ping("localhost"):
591 | pass
592 |
593 | # Should fail with 'getaddrinfo failed':
594 | for val in verbose_ping("foobar_url.fooobar"):
595 | pass
596 |
597 | # Should fail (timeout), but it depends on the local network:
598 | for val in verbose_ping("192.168.255.254"):
599 | pass
600 |
601 | # Should fails with 'The requested address is not valid in its context'
602 | for val in verbose_ping("0.0.0.0"):
603 | pass
604 |
605 | exit()
606 |
607 | parser = argparse.ArgumentParser(prog='python-ping',
608 | description='A pure python implementation\
609 | of the ping protocol. *REQUIRES ROOT*')
610 | parser.add_argument('address', help='The address to attempt to ping.')
611 |
612 | parser.add_argument('-t', '--timeout', help='The maximum amount of time to\
613 | wait until ping timeout.', type=int, default=3000)
614 |
615 | parser.add_argument('-c', '--request_count', help='The number of attempts \
616 | to make. See --infinite to attempt requests until \
617 | stopped.', type=int, default=3)
618 |
619 | parser.add_argument('-i', '--infinite', help='Flag to continuously ping \
620 | a host until stopped.', action='store_true')
621 |
622 | parser.add_argument('-I', '--ipv6', action='store_true', help='Flag to \
623 | use IPv6.')
624 |
625 | parser.add_argument('-s', '--packet_size', type=int, help='Designate the\
626 | amount of data to send per packet.', default=64)
627 |
628 | parser.add_argument('-T', '--test_case', action='store_true', help='Flag \
629 | to run the default test case suite.')
630 |
631 | parser.add_argument('-S', '--source_address', help='Source address from which \
632 | ICMP Echo packets will be sent.')
633 |
634 | parsed = parser.parse_args()
635 |
636 | if parsed.infinite:
637 | sys.exit(list(verbose_ping(parsed.address, parsed.timeout,
638 | None, parsed.packet_size,
639 | ipv6=parsed.ipv6, sourceIP=parsed.source_address))[:-1])
640 |
641 | else:
642 | sys.exit(list(verbose_ping(parsed.address, parsed.timeout,
643 | parsed.request_count, parsed.packet_size,
644 | ipv6=parsed.ipv6, sourceIP=parsed.source_address))[:-1])
645 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | """
5 | distutils setup
6 | ~~~~~~~~~~~~~~~
7 |
8 | :homepage: https://github.com/l4m3rx/python-ping/
9 | :copyleft: 1989-2016 by the python-ping team, see AUTHORS for more details.
10 | :license: GNU GPL v2, see LICENSE for more details.
11 | """
12 |
13 | import os
14 | import subprocess
15 | import sys
16 | import time
17 | import warnings
18 |
19 | from setuptools import setup, find_packages, Command
20 |
21 | PACKAGE_ROOT = os.path.dirname(os.path.abspath(__file__))
22 |
23 |
24 | #VERBOSE = True
25 | VERBOSE = False
26 |
27 | def _error(msg):
28 | if VERBOSE:
29 | warnings.warn(msg)
30 | return ""
31 |
32 | def get_version_from_git():
33 | try:
34 | process = subprocess.Popen(
35 | # %ct: committer date, UNIX timestamp
36 | ["/usr/bin/git", "log", "--pretty=format:%ct-%h", "-1", "HEAD"],
37 | shell=False, cwd=PACKAGE_ROOT,
38 | stdout=subprocess.PIPE, stderr=subprocess.PIPE,
39 | )
40 | except Exception as err:
41 | return _error("Can't get git hash: %s" % err)
42 |
43 | process.wait()
44 | returncode = process.returncode
45 | if returncode != 0:
46 | return _error(
47 | "Can't get git hash, returncode was: %r"
48 | " - git stdout: %r"
49 | " - git stderr: %r"
50 | % (returncode, process.stdout.readline(), process.stderr.readline())
51 | )
52 |
53 | output = process.stdout.readline().strip()
54 | try:
55 | raw_timestamp, mhash = output.split("-", 1)
56 | timestamp = int(raw_timestamp)
57 | except Exception as err:
58 | return _error("Error in git log output! Output was: %r" % output)
59 |
60 | try:
61 | timestamp_formatted = time.strftime("%Y.%m.%d", time.gmtime(timestamp))
62 | except Exception as err:
63 | return _error("can't convert %r to time string: %s" % (timestamp, err))
64 |
65 | return "%s.%s" % (timestamp_formatted, mhash)
66 |
67 |
68 | # convert creole to ReSt on-the-fly, see also:
69 | # https://code.google.com/p/python-creole/wiki/UseInSetup
70 | try:
71 | from creole.setup_utils import get_long_description
72 | except ImportError:
73 | if "register" in sys.argv or "sdist" in sys.argv or "--long-description" in sys.argv:
74 | etype, evalue, etb = sys.exc_info()
75 | evalue = etype("%s - Please install python-creole >= v0.8 - e.g.: pip install python-creole" % evalue)
76 | raise etype(evalue).with_traceback(etb)
77 | long_description = None
78 | else:
79 | long_description = get_long_description(PACKAGE_ROOT)
80 |
81 |
82 | def get_authors():
83 | authors = []
84 | try:
85 | f = file(os.path.join(PACKAGE_ROOT, "AUTHORS"), "r")
86 | for line in f:
87 | if not line.strip().startswith("*"):
88 | continue
89 | if "--" in line:
90 | line = line.split("--", 1)[0]
91 | authors.append(line.strip(" *\r\n"))
92 | f.close()
93 | authors.sort()
94 | except Exception as err:
95 | authors = "[Error: %s]" % err
96 | return authors
97 |
98 |
99 | setup(
100 | name='python-ping',
101 | # version=get_version_from_git(),
102 | version='25102016',
103 | description='A pure python ICMP ping implementation using raw sockets.',
104 | long_description=long_description,
105 | author=get_authors(),
106 | maintainer="Georgi Kolev",
107 | maintainer_email="georgi.kolev@gmail.com",
108 | url='https://github.com/l4m3rx/python-ping/',
109 | keywords="ping icmp network latency",
110 | packages=find_packages(),
111 | include_package_data=True, # include package data under svn source control
112 | zip_safe=False,
113 | scripts=["ping.py"],
114 | classifiers=[
115 | # http://pypi.python.org/pypi?%3Aaction=list_classifiers
116 | # "Development Status :: 4 - Beta",
117 | "Development Status :: 5 - Production/Stable",
118 | "Environment :: Web Environment",
119 | "Intended Audience :: Developers",
120 | "Intended Audience :: Education",
121 | "Intended Audience :: End Users/Desktop",
122 | "License :: OSI Approved :: GNU General Public License (GPL)",
123 | "Operating System :: OS Independent",
124 | "Programming Language :: Python",
125 | "Topic :: Internet",
126 | "Topic :: Software Development :: Libraries :: Python Modules",
127 | "Topic :: System :: Networking :: Monitoring",
128 | ],
129 | test_suite="tests",
130 | )
131 |
--------------------------------------------------------------------------------