csc-cia-stne 0.0.13__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.
@@ -0,0 +1,529 @@
1
+ import requests
2
+ import base64, json
3
+ import os
4
+ from .logger import logger
5
+
6
+ class ServiceNow:
7
+
8
+ def __init__(self, username: str = None, password: str = None, env: str = None) -> None:
9
+ # Verifica se todos os parâmetros foram fornecidos e não estão vazios
10
+ if not all([username and username.strip(), password and password.strip(), env and env.strip()]):
11
+ raise ValueError("Todos os parâmetros precisam ser fornecidos para a criação da instância ServiceNow, ou algum dos valores esta nulo ou vazio (username, password, env)")
12
+
13
+ # Normaliza o valor de 'env' para maiúsculas
14
+ env = env.strip().upper()
15
+
16
+ # Dicionário de ambientes válidos e URLs correspondentes
17
+ valid_envs = {
18
+ 'DEV': 'https://stonedev.service-now.com/api',
19
+ 'QA': 'https://stoneqas.service-now.com/api',
20
+ 'QAS': 'https://stoneqas.service-now.com/api',
21
+ 'PROD': 'https://stone.service-now.com/api'
22
+ }
23
+
24
+ # Verifica se 'env' é válido
25
+ if env not in valid_envs:
26
+ raise ValueError("O valor de 'env' precisa ser 'dev', 'qa', 'qas' ou 'prod'.")
27
+
28
+ # Atribui as variáveis de instância
29
+ self.username = username.strip()
30
+ self.password = password.strip()
31
+ self.env = env
32
+ self.api_url = valid_envs[env]
33
+ self.api_header = {"Content-Type":"application/json","Accept":"application/json"}
34
+
35
+ # Criação de funções básicas para reutilizar nas funções especificas
36
+
37
+ def __auth(self):
38
+ """
39
+ Retorna o e-mail e senha para realizar a autenticação.
40
+ """
41
+ return (self.username, self.password)
42
+
43
+ def request (self, url, params = ""):
44
+ try:
45
+ response = requests.get(
46
+ url,
47
+ params=params,
48
+ auth=self.__auth(),
49
+ headers=self.api_header,
50
+ timeout=15,
51
+ verify=True
52
+ )
53
+
54
+ # VALIDA SE HOUVE SUCESSO NA REQUISIÇÃO
55
+ response.raise_for_status()
56
+
57
+ # Analisa o conteúdo da resposta JSON
58
+ result = response.json()
59
+ if "result" in result:
60
+ return result["result"]
61
+ else:
62
+ logger.warning("A resposta não contém o campo 'result'.")
63
+ return {}
64
+
65
+ except requests.exceptions.HTTPError as http_err:
66
+ logger.error(f"Erro HTTP ao buscar os detalhes do ticket: {http_err}")
67
+ raise
68
+ except requests.exceptions.RequestException as req_err:
69
+ logger.error(f"Erro ao buscar os detalhes do ticket: {req_err}")
70
+ raise
71
+ except Exception as e:
72
+ logger.error(f"Erro inesperado: {e}")
73
+ raise
74
+
75
+ def put(self, url, payload):
76
+
77
+ payload = json.dumps(payload)
78
+
79
+ try:
80
+ response = requests.put(
81
+ f"{url}",
82
+ auth=self.__auth(),
83
+ headers=self.api_header,
84
+ data=f"{payload}",
85
+ timeout=15,
86
+ verify=True
87
+ )
88
+
89
+ # VALIDA SE HOUVE SUCESSO NA REQUISIÇÃO
90
+ response.raise_for_status()
91
+
92
+ # POSSUINDO 'RESULT', TEREMOS O RETORNO DO TICKET ABERTO.
93
+ result = response.json()
94
+ if "result" in result:
95
+ update = result["result"]
96
+ logger.info(f"Atualização concluída com sucesso. Registro atualizado: {update["sys_id"]} | Alterações: {payload}")
97
+ else:
98
+ logger.warning(f"A Resposta da sua requisição não contém o campo 'Result'. Segue o retorno: \n {result} | Alterações: {payload}")
99
+
100
+ #TRATAMENTOS DE ERRO
101
+ except requests.exceptions.HTTPError as http_err:
102
+
103
+ logger.error(f"Erro HTTP ao tentar atualizar o ticket: {http_err} \n Reposta da solicitação: {response.json().get("error").get("message")}")
104
+ raise
105
+ except requests.exceptions.RequestException as req_err:
106
+
107
+ logger.error(f"Erro ao tentar atualizar o ticket: \n {req_err}")
108
+ raise
109
+ except Exception as e:
110
+ logger.error(f"Erro inesperado: \n {e}")
111
+ raise
112
+
113
+ def post(self, url, variables, header_content_type = ""):
114
+ """
115
+ Função para criar um novo ticket no servicenow usando o API REST.
116
+
117
+ Parametros:
118
+ - Payload (Dict): Dicionário contendo os dados que serão utilizados para criar o ticket
119
+ Retorno:
120
+ - Dict: Um dicionário contendo os detalhes do ticket criado
121
+ Raises:
122
+ - Exception: Se ocorrer um erro ao criar o ticket.
123
+
124
+ """
125
+ if header_content_type:
126
+ header = header_content_type
127
+ else:
128
+ header = self.api_header
129
+
130
+ # Ajustar o Payload para a abertura do ticket
131
+ payload = {
132
+ "sysparm_quantity" : "1",
133
+ "variables" : json.dumps(variables)
134
+ }
135
+ try:
136
+ response = requests.post(
137
+ f"{url}",
138
+ auth=self.__auth(),
139
+ headers=header,
140
+ data=f"{payload}"
141
+ )
142
+
143
+ # VALIDA SE HOUVE SUCESSO NA REQUISIÇÃO
144
+ response.raise_for_status()
145
+
146
+ # POSSUINDO 'RESULT', TEREMOS O RETORNO DO TICKET ABERTO.
147
+ result = response.json()
148
+ if "result" in result:
149
+ ticket_number = result["result"].get("number")
150
+ ticket_sys_id = result["result"].get("sys_id")
151
+ logger.info(f"Ticket registrado com sucesso. Número: {ticket_number} | SYS_ID: {ticket_sys_id}")
152
+ return result["result"]
153
+ else:
154
+ logger.warning(f"A Resposta da sua requisição não contém o campo 'Result'. Segue o retorno: \n {result}")
155
+
156
+ #TRATAMENTOS DE ERRO
157
+ except requests.exceptions.HTTPError as http_err:
158
+
159
+ logger.error(f"Erro HTTP ao tentar registrar o ticket: {http_err} \n Reposta da solicitação: {response.json().get("error").get("message")}")
160
+ raise
161
+ except requests.exceptions.RequestException as req_err:
162
+
163
+ logger.error(f"Erro ao tentar registrar o ticket: \n {req_err}")
164
+ raise
165
+ except Exception as e:
166
+ logger.error(f"Erro inesperado: \n {e}")
167
+ raise
168
+
169
+ def listar_tickets(self, tabela: str = None, campos: list = None, query: str = None, limite: int = 50, timeout:int=15)->dict:
170
+ """lista tickets do ServiceNow
171
+
172
+ Args:
173
+ tabela (str): tabela do ServiceNow de onde a query será feita
174
+ campos (list): lista de campos com valores a trazer
175
+ query (str): query do ServiceNow
176
+ limite (int, optional): quantidade máxima de tickets para trazer. Default=50
177
+ timeout (int, optional): segundos para a requisicao dar timeout. Default=15
178
+
179
+ Returns:
180
+ dict: dicionário com o resultado da query
181
+ """
182
+ # Validação de 'tabela'
183
+ if not tabela or not tabela.strip():
184
+ raise ValueError("O parâmetro 'tabela' precisa ser especificado com o nome da tabela de onde os tickets serão listados.")
185
+
186
+ # Validação de 'campos'
187
+ if not isinstance(campos, list) or not campos or not all(isinstance(campo, str) and campo.strip() for campo in campos):
188
+ raise ValueError("O parâmetro 'campos' precisa ser uma lista não vazia de strings válidas com os nomes dos campos do formulário.")
189
+
190
+ # Validação de 'query'
191
+ if not query or not query.strip():
192
+ raise ValueError("O parâmetro 'query' precisa ser uma string não vazia.")
193
+
194
+ # Validação de 'timeout'
195
+ if not isinstance(timeout, int):
196
+ raise ValueError("O parâmetro 'timeout' precisa ser um valor (int) em segundos.")
197
+
198
+ # Validação de 'limite'
199
+ if not isinstance(limite, int):
200
+ raise ValueError("O parâmetro 'limite' precisa ser um número inteiro.")
201
+
202
+ params = {}
203
+ params["sysparm_query"] = query
204
+ params["sysparm_fields"] = ','.join(campos)
205
+ params["sysparm_display_value"] = "all"
206
+ params["sysparm_limit"] = limite
207
+
208
+ url = f"{self.api_url}/now/table/{tabela}"
209
+
210
+ try:
211
+ response = self.request(url, params)
212
+ # response = requests.get(url, params=params, auth=(self.username, self.password), headers=self.api_header, verify=True, timeout=timeout)
213
+ return {"result":True, "content": response.json()}
214
+
215
+ except Exception as e:
216
+ return {}
217
+
218
+ def update_ticket(self, tabela: str = None, sys_id: str = None, campos: dict = None, timeout:int=15)->dict:
219
+ """Atualiza as informações de um ticket
220
+
221
+ Args:
222
+ tabela (str): Tabela do ServiceNow de onde o ticket pertence
223
+ sys_id (str): sys_id do ticket a ser atualizado
224
+ campos (dict): Dicionário com os dados a serem atualizados no ticket
225
+ timeout (int, optional): segundos para a requisicao dar timeout. Default=15
226
+
227
+ Returns:
228
+ dict: resposta do ServiceNow
229
+ """
230
+
231
+ # Validação de 'tabela'
232
+ if not tabela or not tabela.strip():
233
+ raise ValueError("O parâmetro 'tabela' precisa ser especificado com o nome da tabela de onde o ticket será atualizado.")
234
+
235
+ # Validação de 'sys_id'
236
+ if not sys_id or not sys_id.strip():
237
+ raise ValueError("O parâmetro 'sys_id' precisa ser especificado com o sys_id do ticket.")
238
+
239
+ # Validação de 'campos'
240
+ if not isinstance(campos, dict):
241
+ raise ValueError("O parâmetro 'campos' precisa ser um dicionário contendo os campos do ticket a serem atualizados.")
242
+
243
+ if "assigned_to" not in campos:
244
+
245
+ campos["assigned_to"] = self.username
246
+
247
+ try:
248
+
249
+ url = f"{self.api_url}/now/table/{tabela}/{sys_id}"
250
+ response = self.put(url, payload=campos)
251
+ # response = requests.patch(url, auth=(self.username, self.password), headers=self.api_header, data=str(campos).encode('utf-8'), verify=True, timeout=timeout)
252
+
253
+ return response.json()
254
+
255
+ except Exception as e:
256
+
257
+ raise e
258
+
259
+ def anexar_arquivo_no_ticket(self,header_content_type:dict=None, anexo_path:str=None, tabela:str=None, sys_id:str=None, timeout:int=15):
260
+ """Anexa arquivo em um ticket do ServiceNow
261
+
262
+ Args:
263
+ header_content_type (dict): Dicionário contendo a chave 'Content-Type' com a especificação do tipo do arquivo
264
+ anexo_path (str): Path do arquivo a ser anexado
265
+ tabela (str): Tabela do ServiceNow de onde o ticket pertence
266
+ sys_id (str): sys_id do ticket o qual o arquivo será anexado
267
+ timeout (int, optional): segundos para a requisicao dar timeout. Default=15
268
+
269
+ Returns:
270
+ dict: resposta do ServiceNow
271
+ """
272
+
273
+ # Validação de 'tabela'
274
+ if not tabela or not tabela.strip():
275
+ raise ValueError("O parâmetro 'tabela' precisa ser especificado com o nome da tabela do ticket pra onde o arquivo será anexado.")
276
+
277
+ # Validação de 'sys_id'
278
+ if not sys_id or not sys_id.strip():
279
+ raise ValueError("O parâmetro 'sys_id' precisa ser especificado com o sys_id do ticket que o arquivo será anexado.")
280
+
281
+ # Validação de 'anexo_path'
282
+ if not anexo_path or not anexo_path.strip():
283
+ raise ValueError("O parâmetro 'anexo_path' precisa ser especificado com o path para o arquivo a ser anexado.")
284
+
285
+ if not os.path.exists(anexo_path):
286
+ raise FileExistsError(f"O arquivo não foi encontrado ({anexo_path})")
287
+
288
+ header_content_type = self.valida_header_content_type(anexo_path)
289
+
290
+ # Converte as chaves do dicionário para minúsculas
291
+ header_content_type_lower = {k.lower(): v for k, v in header_content_type.items()}
292
+
293
+
294
+
295
+ if "content-type" not in header_content_type_lower:
296
+
297
+ raise ValueError("O parâmetro 'header_content_type' não possui a chave 'Content-Type' com o tipo do anexo")
298
+
299
+ nome_arquivo = os.path.basename(anexo_path)
300
+
301
+ with open(anexo_path, 'rb') as f:
302
+
303
+ file_data = f
304
+
305
+ try:
306
+ url = f"{self.api_url}/now/attachment/file?table_name={tabela}&table_sys_id={sys_id}&file_name={nome_arquivo}"
307
+ response = self.post(url, file_data, header_content_type )
308
+ # response = requests.post(f"{self.api_url}/now/attachment/file?table_name={tabela}&table_sys_id={sys_id}&file_name={nome_arquivo}", headers=header_content_type, auth=(self.username, self.password), data=file_data, timeout=timeout)
309
+ return response.json()
310
+
311
+ except Exception as e:
312
+
313
+ raise e
314
+
315
+ def valida_header_content_type(self, anexo_path ):
316
+
317
+ # Pré validando 'header_content_type'
318
+ if os.path.splitext(anexo_path)[1].lower() == ".zip" and header_content_type is None:
319
+
320
+ header_content_type = {"Content-Type": "application/zip"}
321
+
322
+ elif os.path.splitext(anexo_path)[1].lower() == ".xlsx" and header_content_type is None:
323
+
324
+ header_content_type = {"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}
325
+
326
+ elif os.path.splitext(anexo_path)[1].lower() == ".pdf" and header_content_type is None:
327
+
328
+ header_content_type = {"Content-Type": "application/pdf"}
329
+
330
+ elif os.path.splitext(anexo_path)[1].lower() == ".txt" and header_content_type is None:
331
+
332
+ header_content_type = {"Content-Type": "text/plain"}
333
+
334
+ # Validação de 'header_content_type'
335
+ if not isinstance(header_content_type, dict):
336
+ raise ValueError("O parâmetro 'header_content_type' precisa ser um dicionário contendo o 'Content-Type' do arquivo a ser anexado. (Ex: {\"Content-Type\": \"application/zip\"})")
337
+
338
+ # Validação de 'header_content_type'
339
+ if not isinstance(header_content_type, dict):
340
+ raise ValueError("O parâmetro 'header_content_type' precisa ser um dicionário contendo o 'Content-Type' do arquivo a ser anexado. (Ex: {\"Content-Type\": \"application/zip\"})")
341
+
342
+ return header_content_type
343
+
344
+ def get_anexo(self, sys_id: str = None, tabela: str = None, campo: str = 'default', download_dir:str=None, timeout:int=15)->dict:
345
+ """Traz os anexos de um campo do ticket especificado
346
+
347
+ Args:
348
+ sys_id (str): sys_id do ticket
349
+ tabela (str): tabela do ticket
350
+ campo (str, optional): campo do anexo
351
+ timeout (int, optional): segundos para a requisicao dar timeout. Default=15
352
+
353
+ Returns:
354
+ dict: dicionário com os anexos do ticket
355
+ """
356
+
357
+ # Validação de 'sys_id'
358
+ if not isinstance(sys_id, str) or not sys_id.strip():
359
+ raise ValueError("O parâmetro 'sys_id' precisa ser uma string não vazia com o sys_id do ticket.")
360
+
361
+ # Validação de 'tabela'
362
+ if not isinstance(tabela, str) or not tabela.strip():
363
+ raise ValueError("O parâmetro 'tabela' precisa ser uma string não vazia com o nome da tabela do ticket.")
364
+
365
+ # Validação de 'timeout'
366
+ if not isinstance(timeout, int):
367
+ raise ValueError("O parâmetro 'timeout' precisa ser um valor (int) em segundos.")
368
+
369
+ # Validação de 'download_dir'
370
+ if download_dir is not None:
371
+ if not isinstance(download_dir, str) or not download_dir.strip():
372
+
373
+ raise ValueError("O parâmetro 'download_dir' precisa ser a pasta pra onde o anexo será feito o download.")
374
+
375
+ if not os.path.exists(download_dir):
376
+
377
+ raise NotADirectoryError(f"A pasta informada '{download_dir}' não existe")
378
+
379
+ # Validação de 'campo'
380
+ if not isinstance(campo, str) or not campo.strip():
381
+ raise ValueError("O parâmetro 'campo' precisa ser uma string não vazia com o nome do campo do anexo.")
382
+
383
+ campo = str(campo).strip().lower()
384
+
385
+ # Convert bytes to base64
386
+ def __bytes_to_base64(image_bytes):
387
+ base64_encoded = base64.b64encode(image_bytes)
388
+
389
+ # Decode the bytes to a string (UTF-8 encoding)
390
+ base64_string = base64_encoded.decode('utf-8')
391
+
392
+ return base64_string
393
+
394
+ def __formatar_tamanho(tamanho_bytes):
395
+ # Converte o valor de string para inteiro
396
+ tamanho_bytes = int(tamanho_bytes)
397
+
398
+ # Define os múltiplos de bytes
399
+ unidades = ['B', 'KB', 'MB', 'GB', 'TB']
400
+
401
+ # Itera sobre as unidades até encontrar a maior possível
402
+ for unidade in unidades:
403
+ if tamanho_bytes < 1024:
404
+ return f"{tamanho_bytes:.2f} {unidade}"
405
+ tamanho_bytes /= 1024
406
+
407
+ # Caso o valor seja maior que o esperado (Exabyte ou superior)
408
+ return f"{tamanho_bytes:.2f} PB" # Petabyte
409
+
410
+ anexo_dict = {"var_servicenow": campo, "anexos":[]}
411
+
412
+ # Fazendo download do anexo do campo padrao do ticket
413
+ if campo == 'default':
414
+
415
+ print(f"verificando o campo '{campo}' de anexo")
416
+
417
+ try:
418
+ #url = f"{SERVICENOW_API_URL}/now/attachment?sysparm_query=table_name=sc_req_item^table_sys_id={sys_id}"
419
+ url = f"{self.api_url}/now/attachment?sysparm_query=table_name={tabela}^table_sys_id={sys_id}"
420
+ response = self.request(url)
421
+ # response = requests.get(url, auth=(self.username, self.password), verify=True, timeout=timeout)
422
+ if response.status_code == 200 and len(response.json()["result"]) >= 1:
423
+
424
+ for attachment in response.json()["result"]:
425
+
426
+ print("attachment")
427
+ print(attachment)
428
+ arquivo = {}
429
+ arquivo["file_name"] = attachment["file_name"]
430
+ arquivo["size"] = __formatar_tamanho(attachment["size_bytes"]["display_value"])
431
+ arquivo["content_type"] = attachment["content_type"]
432
+ #arquivo["table_sys_id"] = attachment["table_sys_id"]
433
+
434
+ try:
435
+ byte_response = self.request(attachment["download_link"])
436
+ # byte_response = requests.get(attachment["download_link"], auth=(self.username, self.password), verify=True, timeout=timeout)
437
+
438
+ if byte_response.status_code == 200:
439
+
440
+ arquivo["base64"] = __bytes_to_base64(byte_response.content)
441
+
442
+ if download_dir is not None:
443
+ with open(arquivo["file_name"],'wb') as download_file:
444
+ download_file.write(byte_response.content)
445
+
446
+ #del arquivo["sys_id"]
447
+ #del arquivo["download_link"]
448
+ #del arquivo["table_sys_id"]
449
+ anexo_dict["anexos"].append(arquivo)
450
+ #return JSONResponse(content=arquivo, status_code=byte_response.status_code, media_type="application/json")
451
+ # Requisicao não OK (!= 200)
452
+
453
+ except Exception as e:
454
+
455
+ raise e
456
+
457
+ elif response.status_code != 200:
458
+
459
+ return response.json()
460
+
461
+ except Exception as e:
462
+
463
+ raise e
464
+
465
+ # Fazendo download do anexo do campo especificado do ticket
466
+ else:
467
+
468
+ print(f"verificando o campo '{campo}' de anexo")
469
+
470
+ url = f"{self.api_url}/now/table/{tabela}?sysparm_query=sys_id={sys_id}&sysparm_fields={campo}&sysparm_display_value=all&sysparam_limit=1"
471
+ try:
472
+ response = requests.get(url, auth=(self.username, self.password), verify=True)
473
+
474
+ # Requisicao OK e com anexo
475
+ if response.status_code == 200 and len(response.json()["result"]) >= 1:
476
+
477
+ for ticket_data in response.json()["result"]:
478
+
479
+ if len(ticket_data) >= 1:
480
+
481
+ try:
482
+
483
+ #temp_file_name = ticket_data[campo]["display_value"]
484
+ temp_sys_id = ticket_data[campo]["value"]
485
+
486
+ except Exception as e:
487
+
488
+ raise e
489
+
490
+ url = f"{self.api_url}/now/table/sys_attachment?sysparm_query=sys_id={temp_sys_id}&sysparm_display_value=all&sysparam_limit=1"
491
+
492
+ response = requests.get(url, auth=(self.username, self.password), verify=True)
493
+
494
+ if response.status_code == 200:
495
+
496
+ attachment = response.json()["result"][0]
497
+ arquivo = {}
498
+ arquivo["file_name"] = attachment["file_name"]["display_value"]
499
+ arquivo["size"] = __formatar_tamanho(attachment["size_bytes"]["display_value"])
500
+ #arquivo["sys_id"] = attachment["sys_id"]
501
+ arquivo["content_type"] = attachment["content_type"]["value"]
502
+ #attach_sys_id = attachment["sys_id"]["display_value"]
503
+ #attach_sys_id_table = attachment["table_sys_id"]["value"]
504
+ #url = f"{self.api_url}/now/attachment/{sys_id}/file"
505
+ try:
506
+
507
+ url = f"{self.api_url}/now/attachment/{temp_sys_id}/file"
508
+ byte_response = requests.get(url, auth=(self.username, self.password), verify=True)
509
+ arquivo["base64"] = __bytes_to_base64(byte_response.content)
510
+ anexo_dict["anexos"].append(arquivo)
511
+ if download_dir is not None:
512
+
513
+ with open(arquivo["file_name"],'wb') as download_file:
514
+ download_file.write(byte_response.content)
515
+
516
+ except Exception as e:
517
+
518
+ raise e
519
+
520
+ # Requisicao não OK (!= 200)
521
+ elif response.status_code != 200:
522
+
523
+ return response.json()
524
+
525
+ except Exception as e:
526
+
527
+ raise e
528
+
529
+ return anexo_dict