mirror of
https://github.com/Civlo85/gsmHat.git
synced 2026-07-24 09:46:57 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29d4f54d52 | |||
| 2ffb53977c | |||
| c5ecfc538e | |||
| 5a09095cda | |||
| 4d495905cd | |||
| 76387c74c7 | |||
| 7c3a6ad886 | |||
| 32ba0c839f |
@@ -6,6 +6,8 @@ With gsmHat, you can easily use the functionality of the Waveshare GSM/GPRS/GNSS
|
|||||||
gsmHat was written for Python 3. It provides the following features
|
gsmHat was written for Python 3. It provides the following features
|
||||||
|
|
||||||
- Non-blocking receiving and sending SMS in background
|
- Non-blocking receiving and sending SMS in background
|
||||||
|
- Non-blocking calling
|
||||||
|
- Non-blocking refreshing of actual gps position
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -13,15 +15,14 @@ In the following paragraphs, I am going to describe how you can get and use gsmH
|
|||||||
|
|
||||||
### Getting it
|
### Getting it
|
||||||
|
|
||||||
To download scrapeasy, either fork this github repo or simply use Pypi via pip.
|
To download gsmHat, either fork this github repo or simply use Pypi via pip.
|
||||||
```sh
|
```sh
|
||||||
$ pip3 install gsmHat
|
$ pip3 install gsmHat
|
||||||
```
|
```
|
||||||
|
|
||||||
### Prepare
|
### Prepare
|
||||||
|
|
||||||
* Install your sim card in your module, connect the GSM antenna and mount the module on the pin headers of your Raspberry Pi
|
* Install your sim card in your module, connect the GSM and the GPS antennas and mount the module on the pin headers of your Raspberry Pi. Make sure, that you **do not** need to enter Pin Code to use your card. Pin Codes are not supported yet.
|
||||||
Make sure, that you **do not** need to enter Pin Code to use your card
|
|
||||||
|
|
||||||
* Enable the Uart Interface in your Raspberry Pi
|
* Enable the Uart Interface in your Raspberry Pi
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ $ pip3 install gsmHat
|
|||||||
1. Import gsmHat to your project
|
1. Import gsmHat to your project
|
||||||
|
|
||||||
```Python
|
```Python
|
||||||
from gsmHat import GSMHat, SMS
|
from gsmHat import GSMHat, SMS, GPS
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Init gsmHat
|
2. Init gsmHat
|
||||||
@@ -76,10 +77,57 @@ Message = 'Hello mobile world'
|
|||||||
# Send SMS
|
# Send SMS
|
||||||
gsm.SMS_write(Number, Message)
|
gsm.SMS_write(Number, Message)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
6. Or you can call a number
|
||||||
|
|
||||||
|
```Python
|
||||||
|
Number = '+491601234567'
|
||||||
|
gsm.Call(Number) # This call hangs up automatically after 15 seconds
|
||||||
|
time.sleep(10) # Wait 10 seconds ...
|
||||||
|
gsm.HangUp() # Or you can HangUp by yourself earlier
|
||||||
|
gsm.Call(Number, 60) # Or lets change the timeout to 60 seconds. This call hangs up automatically after 60 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Lets see, where your Raspberry Pi (in a car or in a motocycle or on a cat?) is positioned on earth
|
||||||
|
|
||||||
|
```Python
|
||||||
|
# Get actual GPS position
|
||||||
|
GPSObj = gsm.GetActualGPS()
|
||||||
|
|
||||||
|
# Lets print some values
|
||||||
|
print('GNSS_status: %s' % str(GPSObj.GNSS_status))
|
||||||
|
print('Fix_status: %s' % str(GPSObj.Fix_status))
|
||||||
|
print('UTC: %s' % str(GPSObj.UTC))
|
||||||
|
print('Latitude: %s' % str(GPSObj.Latitude))
|
||||||
|
print('Longitude: %s' % str(GPSObj.Longitude))
|
||||||
|
print('Altitude: %s' % str(GPSObj.Altitude))
|
||||||
|
print('Speed: %s' % str(GPSObj.Speed))
|
||||||
|
print('Course: %s' % str(GPSObj.Course))
|
||||||
|
print('HDOP: %s' % str(GPSObj.HDOP))
|
||||||
|
print('PDOP: %s' % str(GPSObj.PDOP))
|
||||||
|
print('VDOP: %s' % str(GPSObj.VDOP))
|
||||||
|
print('GPS_satellites: %s' % str(GPSObj.GPS_satellites))
|
||||||
|
print('GNSS_satellites: %s' % str(GPSObj.GNSS_satellites))
|
||||||
|
print('Signal: %s' % str(GPSObj.Signal))
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Calculate the distance between two Points on earth
|
||||||
|
|
||||||
|
```Python
|
||||||
|
GPSObj1 = GPS() # You can also use gsm.GetActualGPS() to get an GPS object
|
||||||
|
GPSObj1.Latitude = 52.266949 # Location of Braunschweig, Germany
|
||||||
|
GPSObj1.Longitude = 10.524822
|
||||||
|
|
||||||
|
GPSObj2 = GPS()
|
||||||
|
GPSObj2.Latitude = 36.720005 # Location of Manavgat, Turkey
|
||||||
|
GPSObj2.Longitude = 31.546094
|
||||||
|
|
||||||
|
print('Distance from Braunschweig to Manavgat is [m]:')
|
||||||
|
print(GPS.CalculateDeltaP(GPSObj1, GPSObj2)) # this will print 2384660.7 metres
|
||||||
|
```
|
||||||
|
|
||||||
## What will come in the future?
|
## What will come in the future?
|
||||||
|
|
||||||
* Outgoing and Incoming Calls
|
|
||||||
* GPS functionality
|
|
||||||
* More options to configure the module (e.g. using sim cards with pin code)
|
* More options to configure the module (e.g. using sim cards with pin code)
|
||||||
|
|
||||||
## On which platform was gsmHat developed and tested?
|
## On which platform was gsmHat developed and tested?
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
from gsmHat.gsmHat import GSMHat, SMS
|
from gsmHat.gsmHat import GSMHat, SMS, GPS
|
||||||
+238
-3
@@ -4,6 +4,7 @@ import logging
|
|||||||
import serial
|
import serial
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import RPi.GPIO as GPIO
|
import RPi.GPIO as GPIO
|
||||||
@@ -15,23 +16,57 @@ class SMS:
|
|||||||
self.Receiver = ''
|
self.Receiver = ''
|
||||||
self.Date = ''
|
self.Date = ''
|
||||||
|
|
||||||
|
class GPS:
|
||||||
|
EarthRadius = 6371e3 # meters
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def CalculateDeltaP(Position1, Position2):
|
||||||
|
phi1 = Position1.Latitude * math.pi / 180.0
|
||||||
|
phi2 = Position2.Latitude * math.pi / 180.0
|
||||||
|
deltaPhi = (Position2.Latitude - Position1.Latitude) * math.pi / 180.0
|
||||||
|
deltaLambda = (Position2.Longitude - Position1.Longitude) * math.pi / 180.0
|
||||||
|
|
||||||
|
a = math.sin(deltaPhi / 2) * math.sin(deltaPhi / 2) + math.cos(phi1) * math.cos(phi2) * math.sin(deltaLambda / 2) * math.sin(deltaLambda / 2)
|
||||||
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||||
|
d = GPS.EarthRadius * c # in meters
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.GNSS_status = 0
|
||||||
|
self.Fix_status = 0
|
||||||
|
self.UTC = '' # yyyyMMddhhmmss.sss
|
||||||
|
self.Latitude = 0.0 # ±dd.dddddd [-90.000000,90.000000]
|
||||||
|
self.Longitude = 0.0 # ±ddd.dddddd [-180.000000,180.000000]
|
||||||
|
self.Altitude = 0.0 # in meters
|
||||||
|
self.Speed = 0.0 # km/h [0,999.99]
|
||||||
|
self.Course = 0.0 # degrees [0,360.00]
|
||||||
|
self.HDOP = 0.0 # [0,99.9]
|
||||||
|
self.PDOP = 0.0 # [0,99.9]
|
||||||
|
self.VDOP = 0.0 # [0,99.9]
|
||||||
|
self.GPS_satellites = 0 # [0,99]
|
||||||
|
self.GNSS_satellites = 0 # [0,99]
|
||||||
|
self.Signal = 0.0 # % max = 55 dBHz
|
||||||
|
|
||||||
class GSMHat:
|
class GSMHat:
|
||||||
"""GSM Hat Backend with SMS Functionality (for now)"""
|
"""GSM Hat Backend with SMS Functionality (for now)"""
|
||||||
|
|
||||||
regexGetSingleValue = r'([+][a-zA-Z\ ]+(:\ ))([\d]+)'
|
regexGetSingleValue = r'([+][a-zA-Z\ ]+(:\ ))([\d]+)'
|
||||||
regexGetAllValues = r'([+][a-zA-Z:\s]+)([\w\",\s+\/:]+)'
|
regexGetAllValues = r'([+][a-zA-Z:\s]+)([\w\",\s+\/:.]+)'
|
||||||
timeoutSerial = 5
|
timeoutSerial = 5
|
||||||
|
timeoutGPSActive = 1
|
||||||
|
timeoutGPSInactive = 5
|
||||||
|
|
||||||
def __init__(self, SerialPort, Baudrate):
|
def __init__(self, SerialPort, Baudrate):
|
||||||
self.__baudrate = Baudrate
|
self.__baudrate = Baudrate
|
||||||
self.__port = SerialPort
|
self.__port = SerialPort
|
||||||
|
|
||||||
self.__logger = logging.getLogger(__name__)
|
self.__logger = logging.getLogger(__name__)
|
||||||
self.__logger.setLevel(logging.DEBUG)
|
self.__logger.setLevel(logging.INFO)
|
||||||
self.__loggerFileHandle = logging.FileHandler('gsmHat.log')
|
self.__loggerFileHandle = logging.FileHandler('gsmHat.log')
|
||||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
self.__loggerFileHandle.setFormatter(formatter)
|
self.__loggerFileHandle.setFormatter(formatter)
|
||||||
self.__loggerFileHandle.setLevel(logging.DEBUG)
|
self.__loggerFileHandle.setLevel(logging.INFO )
|
||||||
self.__logger.addHandler(self.__loggerFileHandle)
|
self.__logger.addHandler(self.__loggerFileHandle)
|
||||||
|
|
||||||
self.__connect()
|
self.__connect()
|
||||||
@@ -56,6 +91,16 @@ class GSMHat:
|
|||||||
self.__smsToBuild = None
|
self.__smsToBuild = None
|
||||||
self.__smsList = []
|
self.__smsList = []
|
||||||
self.__smsSendList = []
|
self.__smsSendList = []
|
||||||
|
self.__numberToCall = ''
|
||||||
|
self.__sendHangUp = False
|
||||||
|
self.__startGPS = False
|
||||||
|
self.__GPSstarted = False
|
||||||
|
self.__GPSstartSending = False
|
||||||
|
self.__GPSstopSending = False
|
||||||
|
self.__GPScollectData = False
|
||||||
|
self.__GPSactualData = GPS()
|
||||||
|
self.__GPStimeout = self.timeoutGPSInactive * 1000
|
||||||
|
self.__GPSwaittime = 0
|
||||||
self.__workerThread = threading.Thread(target=self.__workerThread, daemon=True)
|
self.__workerThread = threading.Thread(target=self.__workerThread, daemon=True)
|
||||||
self.__workerThread.start()
|
self.__workerThread.start()
|
||||||
|
|
||||||
@@ -105,6 +150,35 @@ class GSMHat:
|
|||||||
newSMS.Message = Message
|
newSMS.Message = Message
|
||||||
self.__smsSendList.append(newSMS)
|
self.__smsSendList.append(newSMS)
|
||||||
|
|
||||||
|
def Call(self, Number, Timeout = 15):
|
||||||
|
if self.__numberToCall == '':
|
||||||
|
self.__numberToCall = str(Number)
|
||||||
|
self.__callTimeout = Timeout
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def HangUp(self):
|
||||||
|
self.__sendHangUp = True
|
||||||
|
|
||||||
|
def GetActualGPS(self):
|
||||||
|
return self.__GPSactualData
|
||||||
|
|
||||||
|
def __startGPSUnit(self):
|
||||||
|
self.__startGPS = True
|
||||||
|
|
||||||
|
def __startGPSsending(self):
|
||||||
|
self.__GPSstartSending = True
|
||||||
|
|
||||||
|
def __stopGPSsending(self):
|
||||||
|
self.__GPSstopSending = True
|
||||||
|
|
||||||
|
def __collectGPSData(self):
|
||||||
|
self.__GPScollectData = True
|
||||||
|
|
||||||
|
def ColData(self):
|
||||||
|
self.__collectGPSData()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.__disconnect()
|
self.__disconnect()
|
||||||
self.__logger.info('Serial connection to '+self.__port+' closed')
|
self.__logger.info('Serial connection to '+self.__port+' closed')
|
||||||
@@ -172,6 +246,74 @@ class GSMHat:
|
|||||||
self.__logger.debug('New SMS in memory ' + storage + ' at position ' + str(numSMS))
|
self.__logger.debug('New SMS in memory ' + storage + ' at position ' + str(numSMS))
|
||||||
self.__smsToRead = numSMS
|
self.__smsToRead = numSMS
|
||||||
|
|
||||||
|
# GPS Data coming here
|
||||||
|
elif '+CGNSINF:' in self.__serData:
|
||||||
|
self.__logger.debug('New GPS Data:')
|
||||||
|
match = re.findall(self.regexGetAllValues, self.__serData)
|
||||||
|
rawData = match[0][1].split(',')
|
||||||
|
|
||||||
|
newGPS = GPS()
|
||||||
|
|
||||||
|
try:
|
||||||
|
newGPS.GNSS_status = int(rawData[0])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.Fix_status = int(rawData[1])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.UTC = datetime.strptime(rawData[2][:-4], '%Y%m%d%H%M%S')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.Latitude = float(rawData[3])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.Longitude = float(rawData[4])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.Altitude = float(rawData[5])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.Speed = float(rawData[6])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.Course = float(rawData[7])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.HDOP = float(rawData[10])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.PDOP = float(rawData[11])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.VDOP = float(rawData[12])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.GPS_satellites = int(rawData[14])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.GNSS_satellites = int(rawData[15])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
newGPS.Signal = float(rawData[18])/55.0
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.__GPSactualData = newGPS
|
||||||
|
|
||||||
|
|
||||||
self.__serData = ''
|
self.__serData = ''
|
||||||
|
|
||||||
def __waitForUnlock(self):
|
def __waitForUnlock(self):
|
||||||
@@ -222,6 +364,8 @@ class GSMHat:
|
|||||||
actTime = int(round(time.time() * 1000))
|
actTime = int(round(time.time() * 1000))
|
||||||
if self.__state == 1:
|
if self.__state == 1:
|
||||||
if self.__sendToHat('AT+CMGF=1'):
|
if self.__sendToHat('AT+CMGF=1'):
|
||||||
|
self.__startGPSUnit()
|
||||||
|
self.__stopGPSsending()
|
||||||
self.__state = 2
|
self.__state = 2
|
||||||
elif self.__state == 2:
|
elif self.__state == 2:
|
||||||
if self.__waitForUnlock():
|
if self.__waitForUnlock():
|
||||||
@@ -279,15 +423,106 @@ class GSMHat:
|
|||||||
self.__nextState = 2
|
self.__nextState = 2
|
||||||
self.__waitTime = actTime + 5000
|
self.__waitTime = actTime + 5000
|
||||||
|
|
||||||
|
elif self.__state == 40:
|
||||||
|
if self.__sendToHat('ATD' + self.__numberToCall + ';'):
|
||||||
|
self.__state = 41
|
||||||
|
|
||||||
|
elif self.__state == 41:
|
||||||
|
if self.__waitForUnlock():
|
||||||
|
self.__waitTime = actTime + self.__callTimeout * 1000
|
||||||
|
self.__state = 42
|
||||||
|
|
||||||
|
elif self.__state == 42:
|
||||||
|
# Wait x Seconds
|
||||||
|
if actTime > self.__waitTime or self.__sendHangUp == True:
|
||||||
|
self.__numberToCall = ''
|
||||||
|
self.__sendHangUp = True
|
||||||
|
self.__state = 97
|
||||||
|
self.__nextState = 2
|
||||||
|
self.__waitTime = actTime + 5000
|
||||||
|
|
||||||
|
elif self.__state == 43:
|
||||||
|
if self.__sendToHat('AT+CHUP'):
|
||||||
|
self.__state = 44
|
||||||
|
|
||||||
|
elif self.__state == 44:
|
||||||
|
if self.__waitForUnlock():
|
||||||
|
self.__sendHangUp = False
|
||||||
|
self.__state = 97
|
||||||
|
self.__nextState = 2
|
||||||
|
self.__waitTime = actTime + 5000
|
||||||
|
|
||||||
|
elif self.__state == 50:
|
||||||
|
if self.__sendToHat('AT+CGNSPWR=1'):
|
||||||
|
self.__state = 51
|
||||||
|
|
||||||
|
elif self.__state == 51:
|
||||||
|
if self.__waitForUnlock():
|
||||||
|
self.__logger.debug('GPS powered on')
|
||||||
|
self.__startGPS = False
|
||||||
|
self.__state = 97
|
||||||
|
self.__nextState = 2
|
||||||
|
self.__waitTime = actTime + 5000
|
||||||
|
|
||||||
|
elif self.__state == 52:
|
||||||
|
if self.__sendToHat('AT+CGNSTST=1'):
|
||||||
|
self.__state = 55
|
||||||
|
self.__logger.debug('GPS start sending')
|
||||||
|
self.__GPSstartSending = False
|
||||||
|
|
||||||
|
elif self.__state == 53:
|
||||||
|
if self.__sendToHat('AT+CGNSTST=0'):
|
||||||
|
self.__state = 55
|
||||||
|
self.__GPSstopSending = False
|
||||||
|
|
||||||
|
elif self.__state == 54:
|
||||||
|
if self.__sendToHat('AT+CGNSINF'):
|
||||||
|
self.__state = 55
|
||||||
|
self.__GPScollectData = False
|
||||||
|
|
||||||
|
elif self.__state == 55:
|
||||||
|
if self.__waitForUnlock():
|
||||||
|
self.__state = 97
|
||||||
|
self.__nextState = 2
|
||||||
|
self.__waitTime = actTime + 5000
|
||||||
|
|
||||||
elif self.__state == 97:
|
elif self.__state == 97:
|
||||||
# Check if new SMS to send is there
|
# Check if new SMS to send is there
|
||||||
if len(self.__smsSendList) > 0:
|
if len(self.__smsSendList) > 0:
|
||||||
self.__state = 30
|
self.__state = 30
|
||||||
|
|
||||||
|
# Check if we have to Call somebody
|
||||||
|
elif self.__numberToCall != '':
|
||||||
|
self.__state = 40
|
||||||
|
|
||||||
|
# Should I Hang Up ?
|
||||||
|
elif self.__sendHangUp:
|
||||||
|
self.__state = 43
|
||||||
|
|
||||||
# Check if new SMS is there
|
# Check if new SMS is there
|
||||||
elif self.__smsToRead > 0:
|
elif self.__smsToRead > 0:
|
||||||
self.__state = 20
|
self.__state = 20
|
||||||
|
|
||||||
|
# Check if GPS Unit should start
|
||||||
|
elif self.__startGPS:
|
||||||
|
self.__state = 50
|
||||||
|
|
||||||
|
# Check if GPS Unit should start send
|
||||||
|
elif self.__GPSstartSending:
|
||||||
|
self.__state = 52
|
||||||
|
|
||||||
|
# Check if GPS Unit should stop send
|
||||||
|
elif self.__GPSstopSending:
|
||||||
|
self.__state = 53
|
||||||
|
|
||||||
|
# Check if Single GPS Data should be collected
|
||||||
|
elif self.__GPScollectData:
|
||||||
|
self.__state = 54
|
||||||
|
|
||||||
|
elif actTime > self.__GPSwaittime:
|
||||||
|
self.__GPScollectData = True
|
||||||
|
self.__GPSwaittime = actTime + self.__GPStimeout
|
||||||
|
|
||||||
# Wait x Seconds
|
# Wait x Seconds
|
||||||
elif actTime > self.__waitTime:
|
elif actTime > self.__waitTime:
|
||||||
self.__state = self.__nextState
|
self.__state = self.__nextState
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from distutils.core import setup
|
||||||
|
setup(
|
||||||
|
name = 'gsmHat',
|
||||||
|
packages = ['gsmHat'],
|
||||||
|
version = '0.3',
|
||||||
|
license='MIT',
|
||||||
|
description = 'Using the Waveshare GSM/GPRS/GNSS Hat for Raspberry Pi with Python',
|
||||||
|
author = 'Tarek Tounsi',
|
||||||
|
author_email = 'software@tounsi.de',
|
||||||
|
url = 'https://github.com/Civlo85/gsmHat',
|
||||||
|
download_url = 'https://github.com/Civlo85/gsmHat/archive/v_03.tar.gz',
|
||||||
|
keywords = ['Waveshare', 'GSM', 'GPS', 'Raspberry', 'Pi'],
|
||||||
|
install_requires=[
|
||||||
|
'serial',
|
||||||
|
'datetime'
|
||||||
|
],
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 3 - Alpha', # Chose either "3 - Alpha", "4 - Beta" or "5 - Production/Stable" as the current state of your package
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'Topic :: Software Development :: Build Tools',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.4',
|
||||||
|
'Programming Language :: Python :: 3.5',
|
||||||
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
|
],
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user