├── 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 | --------------------------------------------------------------------------------