p3lib 1.1.108__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- p3lib/__init__.py +0 -0
- p3lib/ate.py +108 -0
- p3lib/bokeh_auth.py +363 -0
- p3lib/bokeh_gui.py +845 -0
- p3lib/boot_manager.py +420 -0
- p3lib/conduit.py +145 -0
- p3lib/database_if.py +289 -0
- p3lib/file_io.py +154 -0
- p3lib/gnome_desktop_app.py +146 -0
- p3lib/helper.py +420 -0
- p3lib/json_networking.py +239 -0
- p3lib/login.html +98 -0
- p3lib/mqtt_rpc.py +240 -0
- p3lib/netif.py +226 -0
- p3lib/netplotly.py +223 -0
- p3lib/ngt.py +841 -0
- p3lib/pconfig.py +874 -0
- p3lib/ssh.py +935 -0
- p3lib/table_plot.py +675 -0
- p3lib/uio.py +574 -0
- p3lib-1.1.108.dist-info/LICENSE +21 -0
- p3lib-1.1.108.dist-info/METADATA +34 -0
- p3lib-1.1.108.dist-info/RECORD +24 -0
- p3lib-1.1.108.dist-info/WHEEL +4 -0
p3lib/database_if.py
ADDED
@@ -0,0 +1,289 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
import MySQLdb as mysqldb
|
4
|
+
from datetime import datetime
|
5
|
+
|
6
|
+
class DBConfig(object):
|
7
|
+
"""@brief responsible for holding the attributes if the database configuration."""
|
8
|
+
|
9
|
+
DEFAULT_TCP_PORT = 3306
|
10
|
+
|
11
|
+
def __init__(self):
|
12
|
+
self.serverAddress = ""
|
13
|
+
self.serverPort = DBConfig.DEFAULT_TCP_PORT
|
14
|
+
self.username = ""
|
15
|
+
self.password = ""
|
16
|
+
self.dataBaseName = ""
|
17
|
+
self.uio = None
|
18
|
+
|
19
|
+
class DatabaseIFError(Exception):
|
20
|
+
pass
|
21
|
+
|
22
|
+
class DatabaseIF(object):
|
23
|
+
"""@brief Responsible for providing an interface to a database to allow
|
24
|
+
Creation of database
|
25
|
+
Creation of tables
|
26
|
+
Execution of database sql commands
|
27
|
+
"""
|
28
|
+
|
29
|
+
"""@brief Set this to True to see the SQL statements executed."""
|
30
|
+
DEBUG=False
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
def CheckTableExists(connection, tableName):
|
34
|
+
"""@brief Check if a table exists in the selected database.
|
35
|
+
@param connection The connection to the database.
|
36
|
+
@param tableName The name of the table to check for.
|
37
|
+
@return True if the table exists, False if not."""
|
38
|
+
cursor = connection.cursor()
|
39
|
+
cmd="""SELECT COUNT(*) FROM information_schema.tables WHERE table_name = '{0}'""".format(tableName.replace('\'', '\'\''))
|
40
|
+
cursor.execute(cmd)
|
41
|
+
tableExists = cursor.fetchone()[0] == 1
|
42
|
+
cursor.close()
|
43
|
+
return tableExists
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def GetValidColName(colName):
|
47
|
+
"""@brief Get a valid database column name."""
|
48
|
+
|
49
|
+
VALID_CHAR_LIST = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_"
|
50
|
+
for aChar in colName:
|
51
|
+
if aChar not in VALID_CHAR_LIST:
|
52
|
+
colName = colName.replace(aChar, '_')
|
53
|
+
|
54
|
+
return colName
|
55
|
+
|
56
|
+
@staticmethod
|
57
|
+
def CreateTable(connection, tableName, tableSchemaDict):
|
58
|
+
""""@brief Create a table in the currently USED database..
|
59
|
+
@param connection The connection to the database.
|
60
|
+
@param param:
|
61
|
+
@param tableSchemaDict A python dictionary that defines the table schema.
|
62
|
+
Each dictionary key is the name of the column in the table.
|
63
|
+
Each associated value is the SQL definition of the column type (E.G VARCHAR(64), FLOAT(5,2) etc)."""
|
64
|
+
cursor = connection.cursor()
|
65
|
+
|
66
|
+
sqlCmd = 'CREATE TABLE IF NOT EXISTS `{}` ('.format(tableName)
|
67
|
+
for colName in list(tableSchemaDict.keys()):
|
68
|
+
colDef = tableSchemaDict[colName]
|
69
|
+
correctedColName = DatabaseIF.GetValidColName(colName)
|
70
|
+
sqlCmd = sqlCmd + "`{}` {},\n".format(correctedColName, colDef)
|
71
|
+
|
72
|
+
sqlCmd = sqlCmd[:-2]
|
73
|
+
sqlCmd = sqlCmd + ");"
|
74
|
+
DatabaseIF.ExecuteSQL(connection, cursor, sqlCmd)
|
75
|
+
cursor.close()
|
76
|
+
|
77
|
+
@staticmethod
|
78
|
+
def GetQuotedValue(value):
|
79
|
+
return '\"{}"'.format(str(value))
|
80
|
+
|
81
|
+
@staticmethod
|
82
|
+
def InsertRow(connection, tableName, myDict):
|
83
|
+
"""@brief Insert a row into the table."""
|
84
|
+
cursor = connection.cursor()
|
85
|
+
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
86
|
+
myDict['TIMESTAMP'] = timestamp
|
87
|
+
|
88
|
+
keyList = list(myDict.keys())
|
89
|
+
valueList = []
|
90
|
+
for key in keyList:
|
91
|
+
valueList.append(str(myDict[key]))
|
92
|
+
|
93
|
+
sql = 'INSERT INTO `' + tableName
|
94
|
+
sql += '` ('
|
95
|
+
sql += ', '.join(keyList)
|
96
|
+
sql += ') VALUES ('
|
97
|
+
sql += ', '.join(map(DatabaseIF.GetQuotedValue, valueList))
|
98
|
+
sql += ');'
|
99
|
+
DatabaseIF.ExecuteSQL(connection, cursor, sql)
|
100
|
+
cursor.close()
|
101
|
+
|
102
|
+
@staticmethod
|
103
|
+
def ExecuteSQL(connection, cursor, cmd):
|
104
|
+
try:
|
105
|
+
if DatabaseIF.DEBUG:
|
106
|
+
print('ExecuteSQL(): ',cmd)
|
107
|
+
rowsAffected = cursor.execute(cmd)
|
108
|
+
connection.commit()
|
109
|
+
except:
|
110
|
+
connection.rollback();
|
111
|
+
raise
|
112
|
+
|
113
|
+
return rowsAffected
|
114
|
+
|
115
|
+
@staticmethod
|
116
|
+
def GetInsertableDict(self, rxDict, tableSchema):
|
117
|
+
"""@brief Convert the dict into a dict that can be stored in the database.
|
118
|
+
All the keys must have names that only contain characters that are valid for table columns.
|
119
|
+
Only those keys that are in the tableSchema will be inserted."""
|
120
|
+
keys = list(rxDict.keys())
|
121
|
+
convertedDict = {}
|
122
|
+
for key in keys:
|
123
|
+
colName = DatabaseIF.GetValidColName(key)
|
124
|
+
#Only add the keys that we wish to store in the database
|
125
|
+
if colName in list(tableSchema.keys()):
|
126
|
+
convertedDict[colName] = rxDict[key]
|
127
|
+
return convertedDict
|
128
|
+
|
129
|
+
def __init__(self, config):
|
130
|
+
"""@brief Constructor
|
131
|
+
@param config The database configuration instance."""
|
132
|
+
self._dbConfig = config
|
133
|
+
|
134
|
+
self._dbCon = None
|
135
|
+
|
136
|
+
def _info(self, msg):
|
137
|
+
if self._dbConfig.uio:
|
138
|
+
self._dbConfig.uio.info(msg)
|
139
|
+
|
140
|
+
|
141
|
+
def _debug(self, msg):
|
142
|
+
if self._dbConfig.uio and self._dbConfig.uio.debug:
|
143
|
+
self._dbConfig.uio.debug(msg)
|
144
|
+
|
145
|
+
def connect(self):
|
146
|
+
"""@brief connect to the database server."""
|
147
|
+
self._info("Connecting to {}:{} (database = {})".format(self._dbConfig.serverAddress, self._dbConfig.serverPort, self._dbConfig.dataBaseName))
|
148
|
+
self._dbCon = mysqldb.Connection(host=self._dbConfig.serverAddress,\
|
149
|
+
port=self._dbConfig.serverPort,\
|
150
|
+
user=self._dbConfig.username,\
|
151
|
+
passwd=self._dbConfig.password,\
|
152
|
+
db=self._dbConfig.dataBaseName)
|
153
|
+
self._info("Connected")
|
154
|
+
|
155
|
+
def connectNoDB(self):
|
156
|
+
"""@brief connect to the database server."""
|
157
|
+
self._info("Connecting to database server @ {}:{}".format(self._dbConfig.serverAddress, self._dbConfig.serverPort))
|
158
|
+
self._dbCon = mysqldb.Connection(host=self._dbConfig.serverAddress,\
|
159
|
+
port=self._dbConfig.serverPort,\
|
160
|
+
user=self._dbConfig.username,\
|
161
|
+
passwd=self._dbConfig.password)
|
162
|
+
self._info("Connected")
|
163
|
+
|
164
|
+
def checkTableExists(self, tableName):
|
165
|
+
"""@brief Check if a table exists in any database.
|
166
|
+
@param tableName The name of the table to check for.
|
167
|
+
@return True if the table exists, False if not."""
|
168
|
+
cursor = self._dbCon.cursor()
|
169
|
+
cmd="""SELECT COUNT(*) FROM information_schema.tables WHERE table_name = '{0}'""".format(tableName.replace('\'', '\'\''))
|
170
|
+
self._debug("EXECUTE SQL: {}".format(cmd))
|
171
|
+
#self.executeSQL(cmd)
|
172
|
+
cursor.execute(cmd)
|
173
|
+
tableExists = cursor.fetchone()[0] == 1
|
174
|
+
cursor.close()
|
175
|
+
return tableExists
|
176
|
+
|
177
|
+
def _createTable(self, tableName, tableSchemaDict):
|
178
|
+
""""@brief Create a table in the currently used database..
|
179
|
+
@param tableName The name of the database table.
|
180
|
+
@param tableSchemaDict A python dictionary that defines the table schema.
|
181
|
+
Each dictionary key is the name of the column in the table.
|
182
|
+
Each associated value is the SQL definition of the column type (E.G VARCHAR(64), FLOAT(5,2) etc)."""
|
183
|
+
cursor = self._dbCon.cursor()
|
184
|
+
|
185
|
+
sqlCmd = 'CREATE TABLE IF NOT EXISTS `{}` ('.format(tableName)
|
186
|
+
for colName in list(tableSchemaDict.keys()):
|
187
|
+
colDef = tableSchemaDict[colName]
|
188
|
+
correctedColName = DatabaseIF.GetValidColName(colName)
|
189
|
+
sqlCmd = sqlCmd + "`{}` {},\n".format(correctedColName, colDef)
|
190
|
+
|
191
|
+
sqlCmd = sqlCmd[:-2]
|
192
|
+
sqlCmd = sqlCmd + ");"
|
193
|
+
self.executeSQL(sqlCmd)
|
194
|
+
cursor.close()
|
195
|
+
|
196
|
+
def createTable(self, tableName, tableSchemaDict):
|
197
|
+
"""@brief Create the table in the connected database.
|
198
|
+
@param tableName The table we're innterested in.
|
199
|
+
@param tableSchemaDict The schema for the table in dict form."""
|
200
|
+
self._createTable(tableName, tableSchemaDict)
|
201
|
+
|
202
|
+
def getTableRowCount(self, tableName):
|
203
|
+
"""@brief Get the number of rows in a table.
|
204
|
+
@param tableName The name of the table.
|
205
|
+
@return the number of rows in the table or -1 if not found."""
|
206
|
+
count = -1
|
207
|
+
cmd = "SELECT COUNT(*) as count from {};".format(tableName)
|
208
|
+
retList = self.executeSQL(cmd)
|
209
|
+
if len(retList) > 0:
|
210
|
+
count = retList[0]['count']
|
211
|
+
return count
|
212
|
+
|
213
|
+
def deleteRows(self, tableName, rowCount):
|
214
|
+
"""@brief Delete rows from a table.
|
215
|
+
@param tableName The name of the table.
|
216
|
+
@param rowCount The number of rows to delete."""
|
217
|
+
cmd = "DELETE FROM {} LIMIT {};".format(tableName, rowCount)
|
218
|
+
self.executeSQL(cmd)
|
219
|
+
|
220
|
+
def ensureTableExists(self, tableName, tableSchemaDict, autoCreate):
|
221
|
+
"""@brief Check that the table a table exists in the database.
|
222
|
+
@param tableName The table we're interested in.
|
223
|
+
@param tableSchemaDict The schema for the table in dict form.
|
224
|
+
@param autoCreate Now redundant. It used to be the case that if True then auto create the table."""
|
225
|
+
self._createTable(tableName, tableSchemaDict)
|
226
|
+
|
227
|
+
def insertRow(self, dataDict, tableName, tableSchemaDict):
|
228
|
+
"""@brief Insert a row into a database table.
|
229
|
+
Must have previously connected to the database.
|
230
|
+
@param dataDict The dict holding the table data
|
231
|
+
@param tableName The name of the table to insert data into
|
232
|
+
@param tableSchemaDict The schema for the database table."""
|
233
|
+
insertableDict = DatabaseIF.GetInsertableDict(tableName, dataDict, tableSchemaDict)
|
234
|
+
cursor = self._dbCon.cursor()
|
235
|
+
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
236
|
+
insertableDict['TIMESTAMP'] = timestamp
|
237
|
+
|
238
|
+
keyList = list(insertableDict.keys())
|
239
|
+
valueList = []
|
240
|
+
for key in keyList:
|
241
|
+
valueList.append(str(insertableDict[key]))
|
242
|
+
|
243
|
+
sql = 'INSERT INTO `' + tableName
|
244
|
+
sql += '` ('
|
245
|
+
sql += ', '.join(keyList)
|
246
|
+
sql += ') VALUES ('
|
247
|
+
sql += ', '.join(map(DatabaseIF.GetQuotedValue, valueList))
|
248
|
+
sql += ');'
|
249
|
+
self.executeSQL(sql)
|
250
|
+
cursor.close()
|
251
|
+
|
252
|
+
def executeSQL(self, sqlCmd):
|
253
|
+
"""@brief execute an SQL cmd"""
|
254
|
+
self._debug("EXECUTE SQL: {}".format(sqlCmd))
|
255
|
+
dictCursor = self._dbCon.cursor(mysqldb.cursors.DictCursor)
|
256
|
+
|
257
|
+
dictCursor.execute(sqlCmd)
|
258
|
+
resultDict = dictCursor.fetchall()
|
259
|
+
self._dbCon.commit()
|
260
|
+
return resultDict
|
261
|
+
|
262
|
+
def disconnect(self):
|
263
|
+
"""@brief Disconnect from the database server."""
|
264
|
+
try:
|
265
|
+
if self._dbCon:
|
266
|
+
self._dbCon.close()
|
267
|
+
self._dbCon = None
|
268
|
+
self._info("Disconnected from {}:{}".format(self._dbConfig.serverAddress, self._dbConfig.serverPort))
|
269
|
+
except:
|
270
|
+
pass
|
271
|
+
|
272
|
+
def createDatabase(self):
|
273
|
+
"""@brief Create the database"""
|
274
|
+
sql = "CREATE DATABASE `{}`;".format(self._dbConfig.dataBaseName)
|
275
|
+
self.executeSQL(sql)
|
276
|
+
self._info("Created {} database.".format(self._dbConfig.dataBaseName))
|
277
|
+
|
278
|
+
def dropDatabase(self):
|
279
|
+
"""@brief Delete the database"""
|
280
|
+
sql = "DROP DATABASE `{}`;".format(self._dbConfig.dataBaseName)
|
281
|
+
self.executeSQL(sql)
|
282
|
+
self._info("Deleted {} database.".format(self._dbConfig.dataBaseName))
|
283
|
+
|
284
|
+
def dropTable(self, tableName):
|
285
|
+
"""@brief Delete a table in the database
|
286
|
+
@param tableName The table to drop."""
|
287
|
+
sql = "DROP TABLE `{}`;".format(tableName)
|
288
|
+
self.executeSQL(sql)
|
289
|
+
self._info("Deleted {} database table.".format(tableName))
|
p3lib/file_io.py
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
4
|
+
from cryptography.hazmat.primitives.hashes import SHA256
|
5
|
+
from cryptography.hazmat.backends import default_backend
|
6
|
+
from cryptography.fernet import Fernet
|
7
|
+
from base64 import urlsafe_b64encode
|
8
|
+
|
9
|
+
class CryptFile(object):
|
10
|
+
"""@brief Responsible for encrypting and decrypting data to/from files using a password."""
|
11
|
+
|
12
|
+
def __init__(self,
|
13
|
+
filename: str,
|
14
|
+
password: str,
|
15
|
+
add_enc_extension: bool = True,
|
16
|
+
dict_data: bool = True):
|
17
|
+
"""@brief Constructor
|
18
|
+
@param filename The filename to save the data into.
|
19
|
+
@param password The password to used encrypt data to and load encrypted data from file.
|
20
|
+
@param add_enc_extension If True then the .enc extension is added to the filename
|
21
|
+
supplied.
|
22
|
+
@param dict_data If True the data is a python dictionary."""
|
23
|
+
self._filename = filename
|
24
|
+
self._password = password
|
25
|
+
self._add_enc_extension = add_enc_extension
|
26
|
+
self._dict_data = dict_data
|
27
|
+
|
28
|
+
if not self._filename:
|
29
|
+
raise Exception("No filename defined to save data to.")
|
30
|
+
|
31
|
+
if len(self._filename) < 1:
|
32
|
+
raise Exception("No filename defined. String length = 0.")
|
33
|
+
|
34
|
+
if not self._password:
|
35
|
+
raise Exception("No password defined to encrypt/decrypt data.")
|
36
|
+
|
37
|
+
if len(self._password) < 1:
|
38
|
+
raise Exception("No password defined. String length = 0.")
|
39
|
+
|
40
|
+
self._add_extension()
|
41
|
+
|
42
|
+
def save(self,
|
43
|
+
data):
|
44
|
+
"""@brief Save the data to an encrypted file.
|
45
|
+
@param data The data to be encrypted.
|
46
|
+
"""
|
47
|
+
encrypted_data = self._encrypt_data(data)
|
48
|
+
with open(self._filename, "wb") as file:
|
49
|
+
file.write(encrypted_data)
|
50
|
+
|
51
|
+
def load(self):
|
52
|
+
"""@brief Load data from an encrypted file.
|
53
|
+
@return The decrypted data.
|
54
|
+
"""
|
55
|
+
with open(self._filename, "rb") as file:
|
56
|
+
data_bytes = file.read()
|
57
|
+
return self._decrypt_data(data_bytes)
|
58
|
+
|
59
|
+
def _decrypt_data(self, encrypted_data):
|
60
|
+
# Extract the salt (first 16 bytes) from the encrypted data
|
61
|
+
salt = encrypted_data[:16]
|
62
|
+
encrypted_content = encrypted_data[16:]
|
63
|
+
key = self._derive_key_from_password(salt)
|
64
|
+
fernet = Fernet(key)
|
65
|
+
decrypted_data = fernet.decrypt(encrypted_content)
|
66
|
+
if self._dict_data:
|
67
|
+
# Convert bytes back to a dict
|
68
|
+
data = json.loads(decrypted_data.decode())
|
69
|
+
|
70
|
+
else:
|
71
|
+
data = decrypted_data
|
72
|
+
|
73
|
+
return data
|
74
|
+
|
75
|
+
def get_file(self):
|
76
|
+
"""@return Get the name of the encrypted file."""
|
77
|
+
return self._filename
|
78
|
+
|
79
|
+
def _add_extension(self):
|
80
|
+
"""@brief Add the enc extension to the filename if required."""
|
81
|
+
if self._add_enc_extension and self._filename and not self._filename.endswith('.enc') :
|
82
|
+
self._filename = self._filename + ".enc"
|
83
|
+
|
84
|
+
def _derive_key_from_password(self,
|
85
|
+
salt: bytes) -> bytes:
|
86
|
+
kdf = PBKDF2HMAC(
|
87
|
+
algorithm=SHA256(),
|
88
|
+
length=32,
|
89
|
+
salt=salt,
|
90
|
+
iterations=100_000,
|
91
|
+
backend=default_backend(),
|
92
|
+
)
|
93
|
+
return urlsafe_b64encode(kdf.derive(self._password.encode()))
|
94
|
+
|
95
|
+
def _encrypt_data(self,
|
96
|
+
data):
|
97
|
+
# Generate a random salt for key derivation
|
98
|
+
salt = os.urandom(16)
|
99
|
+
key = self._derive_key_from_password(salt)
|
100
|
+
fernet = Fernet(key)
|
101
|
+
# If we expect a dict
|
102
|
+
if self._dict_data:
|
103
|
+
data_bytes = json.dumps(data).encode() # Convert JSON to bytes
|
104
|
+
|
105
|
+
# else check we have bytes
|
106
|
+
elif isinstance(data, bytes):
|
107
|
+
data_bytes = data
|
108
|
+
|
109
|
+
else:
|
110
|
+
raise Exception("data to be stored is not a bytes instance.")
|
111
|
+
|
112
|
+
encrypted_data = fernet.encrypt(data_bytes)
|
113
|
+
return salt + encrypted_data # Store the salt with the encrypted data
|
114
|
+
|
115
|
+
# Example usage
|
116
|
+
if __name__ == "__main__":
|
117
|
+
|
118
|
+
password = input("Enter a password for encryption: ")
|
119
|
+
|
120
|
+
# JSON data to encrypt
|
121
|
+
json_data = {
|
122
|
+
"name": "Alice",
|
123
|
+
"age": 30,
|
124
|
+
"is_admin": True,
|
125
|
+
"preferences": {
|
126
|
+
"theme": "dark",
|
127
|
+
"language": "English"
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
filename = "afile.txt"
|
132
|
+
|
133
|
+
# Save and load a python dict
|
134
|
+
cjf = CryptFile(filename=filename,
|
135
|
+
password=password)
|
136
|
+
cjf.save(json_data)
|
137
|
+
print(f"Saved {cjf.get_file()}")
|
138
|
+
|
139
|
+
decrypted_data = cjf.load()
|
140
|
+
print(f"Decrypted data: {decrypted_data}")
|
141
|
+
|
142
|
+
|
143
|
+
# Save and load data bytes
|
144
|
+
data_bytes = "123".encode()
|
145
|
+
cjf = CryptFile(filename=filename,
|
146
|
+
password=password,
|
147
|
+
dict_data=False)
|
148
|
+
cjf.save(data_bytes)
|
149
|
+
print(f"Saved {cjf.get_file()}")
|
150
|
+
|
151
|
+
decrypted_data = cjf.load()
|
152
|
+
print("Decrypted data:")
|
153
|
+
for _byte in decrypted_data:
|
154
|
+
print(f"_byte={_byte}")
|
@@ -0,0 +1,146 @@
|
|
1
|
+
#!/bin/env python3
|
2
|
+
|
3
|
+
import os
|
4
|
+
import sys
|
5
|
+
import platform
|
6
|
+
|
7
|
+
class GnomeDesktopApp(object):
|
8
|
+
"""@brief Responsible for adding and removing gnome desktop files for launching applications on a Linux system."""
|
9
|
+
|
10
|
+
@staticmethod
|
11
|
+
def IsLinux():
|
12
|
+
"""@return True if running on a Linux platform."""
|
13
|
+
linux = False
|
14
|
+
if platform.system() == "Linux":
|
15
|
+
linux = True
|
16
|
+
return linux
|
17
|
+
|
18
|
+
def __init__(self, icon_file, app_name=None, comment='', categories='Utility;Application;'):
|
19
|
+
"""@brief Constructor.
|
20
|
+
@param icon_file The name of the icon file. This can be an absolute file name the filename on it's own.
|
21
|
+
If just a filename is passed then the icon file must sit in a folder named 'assets'.
|
22
|
+
This assets folder must be in the same folder as the startup file, it's parent or
|
23
|
+
the python3 site packages folder.
|
24
|
+
@param app_name The name of the application.
|
25
|
+
If not defined then the name of the program executed at startup is used.
|
26
|
+
This name has _ and - character replace with space characters and each
|
27
|
+
word starts with a capital letter.
|
28
|
+
@param comment This comment should detail what the program does and is stored
|
29
|
+
in the gnome desktop file that is created.
|
30
|
+
@param categories The debian app categories. default='Utility;Application;'.
|
31
|
+
"""
|
32
|
+
if not GnomeDesktopApp.IsLinux():
|
33
|
+
raise Exception("The GnomeDesktopApp() class cannot be instantiated on a non Linux platform")
|
34
|
+
self._startup_file = self._get_startup_file()
|
35
|
+
self._gnome_desktop_file = None
|
36
|
+
self._app_name = self._get_app_name()
|
37
|
+
self._check_icon(icon_file)
|
38
|
+
self._comment = comment
|
39
|
+
self._categories = categories
|
40
|
+
self._gnome_desktop_file = self._get_gnome_desktop_file()
|
41
|
+
|
42
|
+
def _get_startup_file(self):
|
43
|
+
"""@return Get the abs name of the program first started."""
|
44
|
+
return os.path.abspath(sys.argv[0])
|
45
|
+
|
46
|
+
def _get_app_name(self, app_name=None):
|
47
|
+
"""@brief Get the name of the app.
|
48
|
+
@param app_name The name of the app or None. If None then the name of the app is the
|
49
|
+
basename of the startup file minus it's extension.
|
50
|
+
@return The name of the app."""
|
51
|
+
if not app_name:
|
52
|
+
# Get just the name of the file
|
53
|
+
app_name = os.path.basename(self._startup_file)
|
54
|
+
# Remove file extension
|
55
|
+
app_name = os.path.splitext(app_name)[0]
|
56
|
+
app_name = app_name.replace('_', ' ')
|
57
|
+
app_name = app_name.replace('-', ' ')
|
58
|
+
app_name = app_name.title()
|
59
|
+
return app_name
|
60
|
+
|
61
|
+
def _check_icon(self, icon_file):
|
62
|
+
"""@brief Check that the icon file exists as this is required for the gnome desktop entry.
|
63
|
+
@param icon_file The name of the icon file.
|
64
|
+
return None"""
|
65
|
+
self._abs_icon_file = os.path.abspath(icon_file)
|
66
|
+
if not os.path.isfile(self._abs_icon_file):
|
67
|
+
startup_path = os.path.dirname(self._startup_file)
|
68
|
+
path1 = os.path.join(startup_path, 'assets')
|
69
|
+
self._abs_icon_file = os.path.join(path1, icon_file)
|
70
|
+
if not os.path.isfile(self._abs_icon_file):
|
71
|
+
startup_parent_path = os.path.join(startup_path, '..')
|
72
|
+
path2 = os.path.join(startup_parent_path, 'assets')
|
73
|
+
self._abs_icon_file = os.path.join(path2, icon_file)
|
74
|
+
if not os.path.isfile(self._abs_icon_file):
|
75
|
+
# Try all the site packages folders we know about.
|
76
|
+
for path in sys.path:
|
77
|
+
if 'site-packages' in path:
|
78
|
+
site_packages_path = path
|
79
|
+
path3 = os.path.join(site_packages_path, 'assets')
|
80
|
+
self._abs_icon_file = os.path.join(path3, icon_file)
|
81
|
+
if os.path.isfile(self._abs_icon_file):
|
82
|
+
return self._abs_icon_file
|
83
|
+
|
84
|
+
raise Exception(f"{self._app_name} icon file ({icon_file}) not found.")
|
85
|
+
|
86
|
+
def _get_gnome_desktop_file(self):
|
87
|
+
"""@brief Determine and return the name of the gnome desktop file.
|
88
|
+
@return The gnome desktop file."""
|
89
|
+
# Get just the name of the file
|
90
|
+
desktop_file_name = os.path.basename(self._startup_file)
|
91
|
+
# Remove file extension
|
92
|
+
desktop_file_name = os.path.splitext(desktop_file_name)[0]
|
93
|
+
if not desktop_file_name.endswith('.desktop'):
|
94
|
+
desktop_file_name = desktop_file_name + '.desktop'
|
95
|
+
home_folder = os.path.expanduser("~")
|
96
|
+
gnome_desktop_apps_folder = os.path.join(home_folder, '.local/share/applications')
|
97
|
+
gnome_desktop_file = os.path.join(gnome_desktop_apps_folder, desktop_file_name)
|
98
|
+
return gnome_desktop_file
|
99
|
+
|
100
|
+
def _create_gnome_desktop_file(self):
|
101
|
+
"""@brief Create the gnome desktop file for this app."""
|
102
|
+
if os.path.isfile(self._gnome_desktop_file):
|
103
|
+
raise Exception(f"{self._gnome_desktop_file} file already exists.")
|
104
|
+
lines = []
|
105
|
+
lines.append('[Desktop Entry]')
|
106
|
+
lines.append('Version=1.0')
|
107
|
+
lines.append('Type=Application')
|
108
|
+
lines.append('Encoding=UTF-8')
|
109
|
+
lines.append(f'Name={self._app_name}')
|
110
|
+
lines.append(f'Comment={self._comment}')
|
111
|
+
lines.append(f'Icon={self._abs_icon_file}')
|
112
|
+
lines.append(f'Exec={self._startup_file}')
|
113
|
+
lines.append('Terminal=false')
|
114
|
+
lines.append(f'Categories={self._categories}')
|
115
|
+
|
116
|
+
with open(self._gnome_desktop_file, "w", encoding="utf-8") as fd:
|
117
|
+
fd.write("\n".join(lines))
|
118
|
+
|
119
|
+
def create(self, overwrite=False):
|
120
|
+
"""@brief Create a desktop icon.
|
121
|
+
@param overwrite If True overwrite any existing file. If False raise an error if the file is already present."""
|
122
|
+
# If this file not found error
|
123
|
+
if not os.path.isfile(self._startup_file):
|
124
|
+
raise Exception(f"{self._startup_file} file not found.")
|
125
|
+
if overwrite:
|
126
|
+
self.delete()
|
127
|
+
self._create_gnome_desktop_file()
|
128
|
+
|
129
|
+
def delete(self):
|
130
|
+
"""@brief Delete the gnome desktop file if present.
|
131
|
+
@return True if a desktop file was deleted."""
|
132
|
+
deleted = False
|
133
|
+
if os.path.isfile(self._gnome_desktop_file):
|
134
|
+
os.remove(self._gnome_desktop_file)
|
135
|
+
deleted = True
|
136
|
+
return deleted
|
137
|
+
|
138
|
+
"""
|
139
|
+
Example usage
|
140
|
+
gda = GnomeDesktopApp('savings.png')
|
141
|
+
gda.create(overwrite=True)
|
142
|
+
"""
|
143
|
+
|
144
|
+
|
145
|
+
|
146
|
+
|