POST
|
Thanks Rhett. I think it was already setup to login automatically as that is the default. I looked into authorizing a license to work offline, which sounds like it'll resolve the situation, but someone was suggesting that the account would need to be admin on the machine it was used and I just figured your solution was a lot easier. So I'm trying this route first. I added some logging and error trapping to your code and it tested out great: # -*- coding: utf-8 -*-
# -------------------------------------------------------------------------
# TouchArcGISPro.py
# Created on: 2020-03-26
# Created by: J....
# Description: Opens local ArcGIS Pro for 60 seconds and then shuts it down.
# Used to keep license from resetting. Run every Sunday evening.
# --------------------------------------------------------------------------
# Import modules
import sys, os, logging, logging.handlers
import time
from datetime import datetime, timedelta
import random
import traceback
def main(date2Run):
# Make a global logging object
logDir = os.path.join(sys.path[0], "logs")
logName = os.path.join(logDir, (date2Run.strftime("TouchPro_%Y-%m-%d_%H-%M.log")))
log = logging.getLogger("TouchPro_" + str(random.random()) )
log.setLevel(logging.INFO)
h1 = logging.FileHandler(logName)
h2 = logging.StreamHandler()
f = logging.Formatter("[%(levelname)s] [%(asctime)s] [%(lineno)d] - %(message)s",'%m/%d/%Y %I:%M:%S %p')
h1.setFormatter(f)
h2.setFormatter(f)
h1.setLevel(logging.INFO)
h2.setLevel(logging.INFO)
log.addHandler(h1)
log.addHandler(h2)
# Start logging
log.info('Script: {0}'.format(os.path.basename(sys.argv[0])))
try:
#Assign the location on the machine, usually here
ArcGIS_Pro_filepath = r"C:\Program Files\ArcGIS\Pro\bin\ArcGISPro.exe"
#Use os.startfile(), this will launch ArcPro
os.startfile(ArcGIS_Pro_filepath)
log.info('ArcGIS Pro was started...')
#Use time.sleep() to wait 60 seconds, enough time for ArcPro to open properly
time.sleep(30)
log.info('...waiting 60 seconds...')
#Use os.system() to forcefully quit named process, ArcGISPro.exe
os.system("taskkill /f /im ArcGISPro.exe")
log.info('ArcGIS Pro process was killed.')
except Exception as e:
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
log.info(e)
log.info("Completed process in {0} script.".format(os.path.basename(sys.argv[0])))
logging.shutdown()
if __name__ == '__main__':
# Run main function
main(datetime.now()) And I created a batch file to start it up, which tested fine in Task Scheduler: "d:\python3\envs\propy05_17_2024\python.exe" "D:\Projects\TouchArcGISPro.py" This is set to run, using Task Scheduler, every Sunday evening. It tested out fine so I'm thinking it will fix my problem. Hopefully I won't have any issues going forward. Thanks again.
... View more
07-26-2024
02:06 PM
|
0
|
0
|
1328
|
POST
|
Oooh, that's cool Rhett. A licensing timeout definitely sounds like what is happening. I'm not sure about how to 'have the sign in automatically selected', and looking at the options in Pro I cannot locate that option. I did notice the option (under licensing) to 'Authorize ArcGIS Pro to work offline', which is disabled since I haven't converted any of my licenses, but maybe that should be the preferred way to ensure timeout doesn't occur? I do like your approach however, so I will try that first. Thanks.
... View more
07-26-2024
10:53 AM
|
0
|
1
|
1336
|
POST
|
I'm not sure if this is the right board, but I'm trying to figure out if my method of running automated Python tasks (using MS Task Scheduler) is correct for ArcGIS Pro (Python 3.x). In our environment, I have a dedicated server where the scripts are stored, and ArcGIS Pro (3.3?) is installed there to get the Python 3.x installed. Pro is licensed through our ArcGIS Online account, and I have a dedicated login just for use with all of the Python scripts. I create a new instance of Python (through Pro) to add libraries (such as Pandas) and save this in a local directory with access permissions to everyone. The Python scripts are run by using a batch file (i.e. DailyAPCDownload.bat) to access the correct Python version and the local py file, and look like: "d:\python3\envs\propy05_17_2024\python.exe" "D:\Projects\Hastus_APC\DailyAPCDownload.py" In the Task Scheduler, the run time is set to daily and the action is set to this batch file which looks like: Every task is set to run using a system account, and to run when the user is not logged in using the highest privileges: The scripts have started to use ArcGIS Python, but still rely a lot on Arcpy, and I call SQL Server stored procedures a lot, as in: # -*- coding: utf-8 -*-
# --------------------------------------------------------------------------------
# DilaxDailyRailDownload.py
# Created on: .....
# Created by: Jeff W......
# Description: Pulls in daily Dilax rail ridership data
# - see T:\Administration\OneNote\VM GIS.one for more information.
# ---------------------------------------------------------------------------
# Import modules
import arcpy, sys, os, xlrd, logging, logging.handlers
from arcgis.gis import GIS
from datetime import datetime, timedelta
from os import walk
import time
import pyodbc
import csv
import fnmatch
import random
import traceback
from email.mime.text import MIMEText
import email.utils
import smtplib
import pandas
import openpyxl
# Describe the Python version, documented at end in GIS_Process_Updates table
arcPyVersion = 3.9
org_url = 'https://www.arcgis.com'
emailHost = "aRelay.valleymetro.com"
emailList = ["someone@valleymetro.com","someoneelse@valleymetro.com"]
emailHeader = " * * * Message from APC data download * * * "
# Credentials to connect to ArcGIS Online
org_url = 'https://www.arcgis.com'
username = 'PlanningVM'
password = 'xyzxy' # password=getpass.getpass("Enter password:")
def emailResults(emailListIn, emailSubject, emailMsg, log):
log.info("Sending emails to %s" % emailListIn)
emailMsg = emailHeader + "\r\n" + emailMsg
for emailID in emailListIn:
try:
emailFrom = "anEmailServer@valleymetro.com"
emailTo = emailID
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
message = emailMsg + now
msg = MIMEText(message, "plain")
msg["Subject"] = emailSubject
msg["Message-id"] = email.utils.make_msgid()
msg["From"] = emailFrom
msg["To"] = emailTo
SMTPserver = smtplib.SMTP(emailHost)
SMTPserver.sendmail(emailFrom, emailTo, msg.as_string())
SMTPserver.quit
except Exception as e:
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
log.info(e)
def main(date2Run):
# Make a global logging object
now = datetime.now()
logDir = os.path.join(sys.path[0], "logs")
reportDir = os.path.join(sys.path[0], "reports")
logName = os.path.join(logDir, (now.strftime("APC_%Y-%m-%d_%H-%M.log")))
finalReport = r"\\w-gis-file\gisdata\projects\APC\BusLoadLast14daysAll.xlsx"
finalCSV = os.path.join(reportDir,"BusLoadLast14daysAll.csv")
log = logging.getLogger("HastusAPC_" + str(random.random()) )
log.setLevel(logging.INFO)
h1 = logging.FileHandler(logName)
h2 = logging.StreamHandler()
f = logging.Formatter("[%(levelname)s] [%(asctime)s] [%(lineno)d] - %(message)s",'%m/%d/%Y %I:%M:%S %p')
h1.setFormatter(f)
h2.setFormatter(f)
h1.setLevel(logging.INFO)
h2.setLevel(logging.INFO)
log.addHandler(h1)
log.addHandler(h2)
# Start logging
log.info('Script: {0}'.format(os.path.basename(sys.argv[0])))
# Set date variables
dToday = date2Run
if len(sys.argv) > 1:
for arg in sys.argv[1:2]:
dToday = datetime.strptime(arg, '%m/%d/%Y')
sToday = dToday.strftime('%m/%d/%Y')
# Parameter for the day to search for:
dlxDay = dToday.strftime('%Y%d%m')
log.info("Generated Day: %s" % dlxDay)
log.info("Processing for %s" % sToday)
print("Processing for %s" % sToday)
# Set scratch geodatabase. T drive for testing, generic for production.
scratchGDB = "%scratchGDB%"
# Parameter for the day to search for:
dlxDay = dToday.strftime('%Y%m%d')
# Establish connection to talk to SQL Server
conn_str = (
r'DRIVER=ODBC Driver 17 for SQL Server;'
r'SERVER=SQL01\GIS;'
r'DATABASE=ridership;'
r'UID=GISAdmin;'
r'PWD=xyzxyzx;'
r'Trusted_Connection=yes;'
)
cnxn = pyodbc.connect(conn_str)
cursor = cnxn.cursor()
# Remove any existing rows for the daily input values
strSQL = "Truncate Table APC_Data_In"
cursor.execute(strSQL)
cnxn.commit()
# Parameter for the input Excel files (as a list)
## APC data is located at
APC_Dir = os.path.join(sys.path[0], "data")
# Check that the directory for the input txt files exist
if not os.path.exists(APC_Dir):
log.info( "Missing APC_Dir")
return 0
files2Process = 0
for (dirpath, dirnames, filenames) in walk(APC_Dir):
try:
dayBegin = date2Run - timedelta(hours=72, minutes=0)
dayEnd = date2Run + timedelta(hours=24, minutes=0)
for filename in fnmatch.filter(filenames, '*.txt'):
newFile = os.path.join(dirpath, filename)
mtime = datetime.fromtimestamp(os.stat(newFile).st_mtime)
if (mtime >= dayBegin and mtime <= dayEnd):
files2Process += 1
readerCSV = csv.reader(open(newFile), delimiter=";")
log.info("Processing %s file" % newFile)
numRows = 1
for row in readerCSV:
if numRows > 0:
strSQL = "INSERT INTO HASTUS_APC_Data_In(BookingID, ShedType, TripID, RouteID, " \
"Direction, BlockID, Rank, StopID, StopDescription, " \
"ScheduleTime, DateMeasured, ArrivalTime, DepartureTime, Boardings, Alightings, Measurement, Source, " \
"LocationObs, TripID_Perm, PlaceID, PassengerLoad) " \
"VALUES ('" + row[0] + "', '" + row[1] + "', '" + row[2] + "', '" + row[3] + "', " \
"'" + row[4] + "', '" + row[5] + "', " + row[6] + ", '" + row[7] + "', '" + row[8].replace('\'', '\'\'') + "', " \
"'" + row[9] + "', '" + row[10] + "', '" + row[11] + "', '" + row[12] + "', " + row[13] + ", " + row[14] + ", '" + row[15] + "', '" + row[16] + "', " \
"'" + row[17] + "', " + row[18] + ", '" + row[20] + "', '" + row[21] + "')"
cursor.execute(strSQL)
cnxn.commit()
numRows += 1
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC Download ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# check that we processed some files, Otherwise, exit the routine
if files2Process > 0:
# Remove any duplicates from the Data In table
log.info("Remove any duplicates (BookingID, TripID, RouteID, StopID, ArrivalTime, or DepatureTime) from the APC_Data_In table")
try:
strSQL = "{CALL [Ridership].[dbo].[ApcInRemoveDuplicates]}"
cursor.execute(strSQL)
cnxn.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC Cleanup ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Calculate the load by TripID
log.info("Update the load by TripID in the APCByTrip table")
try:
strSQL = "{CALL [Ridership].[dbo].[APCLoadByTrip]}"
cursor.execute(strSQL)
cnxn.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC Load By Trip ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Remove any duplicates from the Load by Trip table
log.info("Remove any duplicates (BookingID, TripID, RouteID, StopID, ArrivalTime, or DepatedTime) from the APCByTrip table")
try:
strSQL = "{CALL [Ridership].[dbo].[APCByTripRemoveDuplicates]}"
cursor.execute(strSQL)
cnxn.commit()
except:
# catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC Cleanup Trips ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Move temporary Hastus APC data into permanent table.
log.info("Move temporary APC_Data_In data into APC_Data table")
try:
strSQL = "{CALL [Ridership].[dbo].[APCMakePermanent]}"
cursor.execute(strSQL)
cnxn.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC MakePermanent ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Remove any duplicates from the permanent HASTUS_APC_Data table
log.info("Remove any duplicates (BookingID, TripID, RouteID, StopID, ArrivalTime, or DepatureTime) from the APCByTrip table")
try:
strSQL = "{CALL [Ridership].[dbo].[APCRemoveDuplicates]}"
cursor.execute(strSQL)
cnxn.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC Cleanup Trips ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Export All Bus Load/Ridership Data for the last 14 days
log.info("Exporting All for the last 14 days")
try:
param = ('All')
strSQL = "{CALL [Ridership].[dbo].[APCGetBusLoadLast14] (?)}"
cursor.execute(strSQL, param)
cnxn.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC All Export ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Export to Excel file
BusLoadTable = os.path.join(sys.path[0], r"Ridership on W-SQL01.sde\Ridership.DBO.BusLoadOutput")
OutExcelAll = os.path.join(reportDir, "BusLoadLast14daysAll.xls")
if os.path.exists(OutExcelAll):
os.remove(OutExcelAll)
arcpy.conversion.TableToExcel(Input_Table=BusLoadTable, Output_Excel_File=OutExcelAll)
# Prepare the All inclusive Excel file using Pandas
if os.path.exists(finalReport):
os.remove(finalReport)
outWriter = pandas.ExcelWriter(finalReport, engine='openpyxl')
# Pandas
cnxn2 = pyodbc.connect(conn_str)
sqlStr = "Select * From Ridership.DBO.BusLoadOutput"
data = pandas.read_sql(sqlStr, cnxn2)
cnxn2.commit()
Data = pandas.DataFrame(data)
print(Data.columns.tolist())
print(finalReport)
Data.sort_values(by=['ArriveDate', 'RouteID'])
Data.to_excel(finalReport, sheet_name="All")
Data.to_excel(outWriter, sheet_name="All") #, index=False, engine='xlsxwriter')
Data.to_csv(finalCSV, index=False)
# Export Monday Data
log.info("Exporting Mondays for the last 14 days")
try:
param = ('Monday')
strSQL = "{CALL [Ridership].[dbo].[HASTUSGetBusLoadLast14] (?)}"
cursor.execute(strSQL, param)
cnxn.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC Monday Export ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Export to Excel file
BusLoadTable = os.path.join(sys.path[0], r"Ridership on W-SQL01.sde\Ridership.DBO.BusLoadOutput")
OutExcelMon = os.path.join(reportDir, "BusLoadLast14daysMon.xls")
if os.path.exists(OutExcelMon):
os.remove(OutExcelMon)
arcpy.conversion.TableToExcel(Input_Table=BusLoadTable, Output_Excel_File=OutExcelMon)
# Pandas
sqlStr = "Select * From Ridership.DBO.BusLoadOutput"
data = pandas.read_sql(sqlStr, cnxn2)
cnxn2.commit()
Data = pandas.DataFrame(data)
Data.to_excel(outWriter, sheet_name="Monday")
# Export Tuesday Data
log.info("Exporting Tuesdays for the last 14 days")
try:
param = ('Tuesday')
strSQL = "{CALL [Ridership].[dbo].[APCGetBusLoadLast14] (?)}"
cursor.execute(strSQL, param)
cnxn.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC Tuesday Export ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Export to Excel file
BusLoadTable = os.path.join(sys.path[0], r"Ridership on W-SQL01.sde\Ridership.DBO.BusLoadOutput")
OutExcelTues = os.path.join(reportDir, "BusLoadLast14daysTues.xls")
if os.path.exists(OutExcelTues):
os.remove(OutExcelTues)
arcpy.conversion.TableToExcel(Input_Table=BusLoadTable, Output_Excel_File=OutExcelTues)
# Pandas
sqlStr = "Select * From Ridership.DBO.BusLoadOutput"
data = pandas.read_sql(sqlStr, cnxn2)
cnxn2.commit()
Data = pandas.DataFrame(data)
Data.to_excel(outWriter, sheet_name="Tuesday") #, index=false, engine='openpyxl')
# Export Wednesday Data
log.info("Exporting Wednesdays for the last 14 days")
try:
param = ('Wednesday')
strSQL = "{CALL [Ridership].[dbo].[APCGetBusLoadLast14] (?)}"
cursor.execute(strSQL, param)
cnxn.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC Wednesday Export ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Export to Excel file
BusLoadTable = os.path.join(sys.path[0], r"Ridership on W-SQL01.sde\Ridership.DBO.BusLoadOutput")
OutExcelWed = os.path.join(reportDir, "BusLoadLast14daysWed.xls")
if os.path.exists(OutExcelWed):
os.remove(OutExcelWed)
arcpy.conversion.TableToExcel(Input_Table=BusLoadTable, Output_Excel_File=OutExcelWed)
# Pandas
sqlStr = "Select * From Ridership.DBO.BusLoadOutput"
data = pandas.read_sql(sqlStr, cnxn2)
cnxn2.commit()
Data = pandas.DataFrame(data)
Data.to_excel(outWriter, sheet_name="Wednesday") #, index=false, engine='openpyxl')
# Export Thursday Data
log.info("Exporting Thursdays for the last 14 days")
try:
param = ('Thursday')
strSQL = "{CALL [Ridership].[dbo].[APCGetBusLoadLast14] (?)}"
cursor.execute(strSQL, param)
cnxn.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC Thursday Export ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Export to Excel file
BusLoadTable = os.path.join(sys.path[0], r"Ridership on W-SQL01.sde\Ridership.DBO.BusLoadOutput")
OutExcelThurs = os.path.join(reportDir, "BusLoadLast14daysThurs.xls")
if os.path.exists(OutExcelThurs):
os.remove(OutExcelThurs)
arcpy.conversion.TableToExcel(Input_Table=BusLoadTable, Output_Excel_File=OutExcelThurs)
# Pandas
sqlStr = "Select * From Ridership.DBO.BusLoadOutput"
data = pandas.read_sql(sqlStr, cnxn2)
cnxn2.commit()
Data = pandas.DataFrame(data)
Data.to_excel(outWriter, sheet_name="Thursday") #, index=false, engine='openpyxl')
# Export Friday data
log.info("Exporting Fridays for the last 14 days")
try:
param = ('Friday')
strSQL = "{CALL [Ridership].[dbo].[APCGetBusLoadLast14] (?)}"
cursor.execute(strSQL, param)
cnxn.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC Friday Export ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Export to Excel file
BusLoadTable = os.path.join(sys.path[0], r"Ridership on W-SQL01.sde\Ridership.DBO.BusLoadOutput")
OutExcelFri = os.path.join(reportDir, "BusLoadLast14daysFri.xls")
if os.path.exists(OutExcelFri):
os.remove(OutExcelFri)
arcpy.conversion.TableToExcel(Input_Table=BusLoadTable, Output_Excel_File=OutExcelFri)
# Pandas
sqlStr = "Select * From Ridership.DBO.BusLoadOutput"
data = pandas.read_sql(sqlStr, cnxn2)
cnxn2.commit()
Data = pandas.DataFrame(data)
Data.to_excel(outWriter, sheet_name="Friday") #, index=false, engine='openpyxl')
# Export Weekend data
log.info("Exporting Saturday and Sundays for the last 14 days")
try:
param = ('Weekend')
strSQL = "{CALL [Ridership].[dbo].[APCGetBusLoadLast14] (?)}"
cursor.execute(strSQL, param)
cnxn.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "APC Weekend Export ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn.close()
sys.exit(0)
# Export to Excel file
BusLoadTable = os.path.join(sys.path[0], r"Ridership on W-SQL01.sde\Ridership.DBO.BusLoadOutput")
OutExcelWeekend = os.path.join(reportDir, "BusLoadLast14daysWeekend.xls")
if os.path.exists(OutExcelWeekend):
os.remove(OutExcelWeekend)
arcpy.conversion.TableToExcel(Input_Table=BusLoadTable, Output_Excel_File=OutExcelWeekend)
# Pandas
sqlStr = "Select * From Ridership.DBO.BusLoadOutput"
data = pandas.read_sql(sqlStr, cnxn2)
cnxn2.commit()
Data = pandas.DataFrame(data)
Data.to_excel(outWriter, sheet_name="Weekend") #, index=false, engine='openpyxl')
cnxn2.close()
outWriter.close()
cnxn.close()
log.info(".Sending email")
newPre = datetime.now().strftime("%Y%m%d")
log.info(newPre)
msgDiff = "\r\n................................................................." + "\r\r\n"
msgDiff += "Download of the latest HASTUS APC data is successful" + "\r\r\n"
msgDiff += "Processing of HASTUS APC data is successful for %s" % sToday + "\r\r\n"
msgDiff += "Processed data file is available at %s" % finalReport + "\r\r\n"
msgDiff += "Process was completed on '" + newPre + " \r\r\n"
msgDiff += "................................................................." + " \r\r\n"
log.info(msgDiff)
emailResults(emailList, "APC Download Successful", msgDiff, log)
log.info("...Processing of APC data is successful for %s" % sToday)
#Write information to the GIS_Process_Updates table
log.info("...Writing results to GIS_Process_Updates table")
cnxn3 = pyodbc.connect(conn_str)
cursor3 = cnxn3.cursor()
dToday = datetime.now() # + timedelta(hours=7)
sToday = dToday.strftime('%m/%d/%Y %H:%M:%S')
script = 'DailyAPCDownload.py'
process = 'APC Daily Download'
calledBy = 'DailyAPCDownload.bat'
message = 'Successful'
try:
strSQL = "INSERT INTO [PortalData].[dbo].[GIS_Process_Updates]([DateEntered],[DateUpdated],[ProcessName],[ScriptName],[CalledBy],[Message],[ArcPyVersion],[CheckStatus])"
strSQL += "VALUES('{0}','{1}','{2}','{3}','{4}','{5}','{6}', {7})".format(sToday, sToday, process, script, calledBy, message, arcPyVersion, -1)
cursor3.execute(strSQL)
cnxn3.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "Daily bus download error", traceback.format_exc(), log)
logging.shutdown()
cnxn3.close()
sys.exit(0)
# Move Bus ridership values into the aggregate table DilaxBusData
log.info("One last call to update Bus volume information")
try:
strSQL = "{CALL [Ridership].[dbo].[Generate30DayBusRailVolumes]}"
cursor3.execute(strSQL)
cnxn3.commit()
except: # catch *all* exceptions, close SQL connection, and stop Python execution
log.info("** Error: %s" % sys.exc_info()[0])
log.info(traceback.format_exc())
emailResults(emailList, "Daily Bus Volume ERROR", traceback.format_exc(), log)
logging.shutdown()
cnxn3.close()
sys.exit(0)
cnxn3.close()
log.info("Completed process in {0} script.".format(script))
logging.shutdown()
return 0
if __name__ == '__main__':
# Run main function to pull data and process it
main(datetime.strptime(datetime.now().strftime("%m/%d/%Y"), '%m/%d/%Y'))
# Terminate all processes
sys.exit(0)
I know it's a lot, but just for the example of how I used ArcPy, ArcGIS.GIS, and Pyodbc for connecting to SQL Server stored procedures (and sometimes being lazy and doing the SQL inline). I'm sure some of this could be cleaned up, but it's working, when Task Scheduler runs the program. Under ArcMap, and Python 2.7 I rarely experienced an issue, but now that it's relying on Pro, every once in awhile I need to open Pro, check the Python environment ( a couple of times the created one went bad for some reason) and then close the application. Then everything works fine. On July 13th something happened to the server and it rebooted. I was gone for a week, but when I returned none of the scripts were being run by Task Scheduler, and none had run since 7/13. I logged in as myself, and had no issue with running them through IDLE (for Pro), or through the BAT file. I logged into Pro and checked the Python environment and everything seemed fine, but nothing was running in Task Scheduler. Only after logging into the server using the service account, and then opening Pro and checking the environment, did things start working in Task Scheduler. So my question (thanks for reading this far, I know this is supposed to be higher up, but I thought the buildup would help) is: Does this setup seem like a reasonable way to run tasks automatically? We have multiple scripts running every day and this new way of using ArcGIS Pro seems a little too sensitive. I wish there was someway to set the access and forget it, but maybe the accessing of the Pro license through ArcGIS Online should be localized? I've looked through the help and only discovered these 2 questions that are close (and relatively recent), but they aren't exactly answering my question as to the best way to do this: https://community.esri.com/t5/arcgis-pro-questions/run-pro-scheduled-task-with-service-account/m-p/1385617 https://community.esri.com/t5/arcgis-pro-questions/task-scheduler-for-gis-tasks-skipping-python/m-p/1413583 If anyone has any suggestions I would love to hear them. Thanks
... View more
07-25-2024
09:57 AM
|
0
|
4
|
1397
|
POST
|
Here's an example from Esri on how to setup a Popup Template: https://developers.arcgis.com/javascript/latest/sample-code/popuptemplate-function/ I opened the Code Pen and changed the populationChange function to: function populationChange(feature) {
const ogId = feature.graphic.attributes.STATE_NAME // Added
const div = document.createElement("div");
const upArrow =
'<svg width="16" height="16" ><polygon points="14.14 7.07 7.07 0 0 7.07 4.07 7.07 4.07 16 10.07 16 10.07 7.07 14.14 7.07" style="fill:green"/></svg>';
const downArrow =
'<svg width="16" height="16"><polygon points="0 8.93 7.07 16 14.14 8.93 10.07 8.93 10.07 0 4.07 0 4.07 8.93 0 8.93" style="fill:red"/></svg>';
// Calculate the population percent change from 2010 to 2013.
const diff = feature.graphic.attributes.POP2013 - feature.graphic.attributes.POP2010;
const pctChange = (diff * 100) / feature.graphic.attributes.POP2010;
const arrow = diff > 0 ? upArrow : downArrow;
// Add green arrow if the percent change is positive and a red arrow for negative percent change.
div.innerHTML =
"As of 2010, the total population in this area was <b>"+feature.graphic.attributes.POP2010+"</b> and the density was <b>"+feature.graphic.attributes.POP10_SQMI+"</b> sq mi. As of 2013, the total population was <b>"+feature.graphic.attributes.POP2013+"</b> and the density was <b>"+feature.graphic.attributes.POP13_SQMI+"</b> sq mi. <br/> <br/>" +
"Percent change is " +
arrow +
"<span style='color: " +
(pctChange < 0 ? "red" : "green") +
";'>" +
pctChange.toFixed(3) +
"%</span>" +
"<br /><a href='https://services.arcgis.com/V6ZHFr6zdgNZuVG0/ArcGIS/rest/services/US_Counties/FeatureServer/0/query?where=State_Name%3D%27" + ogId + "%27&outFields=*&f=html' target='_blank'>Test Me</a>"; // Added
return div;
}; So I set 'ogId' to be an attribute that is not part of the popup, and you can see all of the attributes here: https://www.arcgis.com/home/item.html?id=e8f85b4982a24210b9c8aa20ba4e1bf7#overview But you can get access to a field for the selected graphic that is not part of the popup, which is what I think you are after.
... View more
03-11-2024
12:39 PM
|
0
|
1
|
1023
|
POST
|
I'm not sure I'm following. If you want to not have things in the popup, then just leave them out, you control that, as in: const popupCase = {
"title": "Address: {Property_Address}",
"content": [{
type: "text",
text: "<ul><li><b>Description:</b> {CaseDesc}<br></li>"+
"<li><b>Status:</b> {CaseStatus}<br></li>"+
"<li><b>Date of Infraction:</b> {DateOfInfraction}<br></li>"+
"<li><b>Inspector:</b> {InspectorFullName}<br></li>"+
"<li><b>Owner Name:</b> {Property_OwnerName}<br></li>"+
"<li><b>Legal Description</b> {Property_LegalDescription}<br></li>"+
"<li><b>Narrative</b> {Narrative}<br></li>"
}],
actions: [openSphinxAction,openPaAction]
}; Or, if you want to keep them in the popup I think you can just modify the display style to 'none', as in: const popupCase = {
"title": "Address: {Property_Address}",
"content": [{
type: "text",
text: "<ul><li><b>Description:</b> {CaseDesc}<br></li>"+
"<li><b>Status:</b> {CaseStatus}<br></li>"+
"<li><b>Date of Infraction:</b> {DateOfInfraction}<br></li>"+
"<li><b>Inspector:</b> {InspectorFullName}<br></li>"+
"<li><b>Owner Name:</b> {Property_OwnerName}<br></li>"+
"<li><b>Legal Description</b> {Property_LegalDescription}<br></li>"+
"<li><b>Narrative</b> {Narrative}<br></li>"+
"<li style="display:none;"><b>OriginalID:</b> {OriginalId}<br></li>"+
"<li style="display:none;"><b>Property Id:<\b> {Property_PropertyId}</li></ul>"
}],
actions: [openSphinxAction,openPaAction]
}; I recently did this just to test some popup output, without losing what was there. Not sure why else you would use it. Or you can modify the REST call to query out the feature information. I think this last one is not what you are looking for as you want the ID info, you just don't want to display it.
... View more
03-11-2024
10:11 AM
|
0
|
3
|
1049
|
POST
|
Oh, and look at the Console for the results. I just used the Esri sample for promises, but I cleaned most of it out and never got the textArea to work well. But the data's viewable in the console.
... View more
02-28-2024
01:45 PM
|
0
|
0
|
2037
|
POST
|
I went back and looked what I had done for 4.24, and it relied on Dojo promises. Esri just talks about the .then/.catch method of working with promises, but in a script I had developed I as passing the promise (deferred) among multiple functions and only after successfully completing the last one did I resolve (or reject) the promise. Then, the program waited for all function calls to finish for all records, which as accomplished using the Dojo 'All' function. Looks like this is not available anymore as 4.29 now seems devoid of all Dojo, and after looking at possibly adding it to the libraries, I thought the better of it and decided I should figure out how to do this with native Javascript promises. This may not be the correct way, but it works. You just have to parse through all of the resulting features for each call to get (in this case) the value for 'Hispanic': <html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>Request data from a remote server | Sample | ArcGIS Maps SDK for JavaScript 4.29</title>
<link rel="stylesheet" href="https://js.arcgis.com/4.29/esri/themes/light/main.css" />
<script src="https://js.arcgis.com/4.29/"></script>
<script type="module" src="https://js.arcgis.com/calcite-components/2.5.1/calcite.esm.js"></script>
<link rel="stylesheet" type="text/css" href="https://js.arcgis.com/calcite-components/2.5.1/calcite.css" />
</head>
<body>
<calcite-shell>
<calcite-panel heading="Using esri/request">
<calcite-block
heading="Enter a URL:"
description='Enter a server URL to the input box below, then click the "Make Request" button to send a request.'
open
>
<calcite-input-text
id="input-url"
placeholder="https://services.arcgisonline.com/arcgis/rest/services/World_Topo_Map/MapServer"
value="https://services.arcgisonline.com/arcgis/rest/services/World_Topo_Map/MapServer"
required
></calcite-input-text>
<calcite-button id="request-button" style="padding-top: 5px">Make Request</calcite-button>
<calcite-label style="padding-top: 10px" scale="l"
>Server response from the request:
<calcite-text-area
id="results-text-area"
placeholder="Request response will show here.."
read-only
rows="25"
></calcite-text-area>
</calcite-label>
</calcite-block>
</calcite-panel>
</calcite-shell>
<calcite-alert id="alert" kind="danger" icon label="Danger alert" auto-close>
<div slot="title">Enter a valid URL.</div>
</calcite-alert>
<script>
require(["esri/request"], (esriRequest) => {
const requestButton = document.getElementById("request-button");
const urlInput = document.getElementById("input-url");
const textArea = document.getElementById("results-text-area");
const alert = document.getElementById("alert");
let url;
let sStates = ['Arizona', 'California', 'Nevada']
let promiseStates = [];
let sResults = '';
requestButton.addEventListener("click", () => {
//var variable = null;
for (let j = 0; j < sStates.length; j++) {
let thestate = sStates[j];
const aDef = new Promise((resolve, reject) => {
get_results(thestate, resolve, reject);
});
promiseStates.push(aDef);
}
Promise.all(promiseStates).then((values) => {
console.log("in Promise.all");
console.log(values);
textArea.value += sResults;
});
});
async function get_results(state, pRes, pRej) {
console.log(state);
let getinfo =
"https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/2/query?f=json&Where=STATE_NAME='";
params = state;
params += "'&outFields=NAME,HISPANIC";
getinfo = getinfo + encodeURI(params);
var options = {
query: {
f: "json",
},
responseType: "json",
};
console.log(getinfo);
console.log()
await esriRequest(getinfo, options).then((response) => {
console.log("in esriRequest");
console.log(response.data);
sResults += response.data.fields["Name"];
return pRes(response);
})
.catch(function (error) {
console.error("esriRequest Error: " + error);
textArea.value = error;
return pRej(error);
});
}
});
</script>
</body>
</html> (oh, and it uses your original function for calling the REST service).
... View more
02-28-2024
01:40 PM
|
1
|
0
|
2037
|
POST
|
I'm not sure what you need to set an await for, just handling the Esri Request response should be sufficient, as in: function get_results(state) {
let getinfo =
"https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/2/query?f=json&Where=STATE_NAME='";
params = state;
params += "'&outFields=NAME,HISPANIC";
getinfo = getinfo + encodeURI(params);
esriRequest(getinfo, {
responseType: "json"options
}).then(function (response) {
// Do something like
let thisVal = response.data.fields[0].domain.codedValues;
optionsSelector.push({value: thisVal, label thisVal.name});
console.log(response);
}).catch(function (error) {
console.error("esriRequest Error: " + error);
});
});
} But if you need to wait for something you can always push that into a promise, like: function get_results(state) {
let defStateQuery = new Deferred();
let promiseQueries = []
promiseQueries.push(defStateQuery)
setTimeout(function (inState) {
let getinfo = "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/2/query?f=json&Where=STATE_NAME='";
params = inState;
params += "'&outFields=NAME,HISPANIC";
getinfo = getinfo + encodeURI(params);
//var options = { query: {f: "json",}, responseType: "json"};
esriRequest(getinfo, {
responseType: "json"options
}).then(function (response) {
// Do something like
let thisVal = response.data.fields[0].domain.codedValues;
optionsSelector.push({value: thisVal, label thisVal.name});
console.log(response);
deferred.resolve(response);
return deferred.promise
}).catch(function (error) {
console.error("esriRequest Error: " + error);
deferred.reject(error);
return deferred.promise;
});
});
}, 0);
return deferred.promise;
}
all(promiseQueries).then(function (results) {
console.log(results);
}); This sample allows you to enter a URL and see the results, but the REST service seems to not be working (maybe there's too many of us trying to write a response, LOL). https://developers.arcgis.com/javascript/latest/sample-code/request/
... View more
02-28-2024
11:12 AM
|
0
|
1
|
2063
|
POST
|
I think that's what you'd have to do if you utilized the Search Widget, and I don't know about the performance of 60+ layers but it'd probably work at least. If you have only one (or a small number of ) field to search in each layer maybe you could mimic the Search Widget by using the where clause directly on the REST web service, then you could compile the possible features selected in whatever way you wanted from the resulting JSON, as in this REST call: https://services2.arcgis.com/2t1927381mhTgWNC/arcgis/rest/services/BusStopsWAmenities/FeatureServer/0/query?where=stopID+%3D+5655&objectIds=&time=&geometry=&geometryType=esriGeometryEnvelope&inSR=&spatialRel=esriSpatialRelIntersects&resultType=none&distance=0.0&units=esriSRUnit_Meter&relationParam=&returnGeodetic=false&outFields=*&returnGeometry=true&featureEncoding=esriDefault&multipatchOption=xyFootprint&maxAllowableOffset=&geometryPrecision=&outSR=&defaultSR=&datumTransformation=&applyVCSProjection=false&returnIdsOnly=false&returnUniqueIdsOnly=false&returnCountOnly=false&returnExtentOnly=false&returnQueryGeometry=false&returnDistinctValues=false&cacheHint=false&orderByFields=&groupByFieldsForStatistics=&outStatistics=&having=&resultOffset=&resultRecordCount=&returnZ=false&returnM=false&returnExceededLimitFeatures=true&quantizationParameters=&sqlFormat=none&f=html&token=
... View more
02-27-2024
01:13 PM
|
2
|
1
|
1149
|
POST
|
Per this page (https://doc.arcgis.com/en/arcgis-online/reference/supported-html.htm ) which is for ArcGIS Online, I wonder if the 'button' tag is no longer accepted as an HTML Element. I was able to get an IMG tag to work by dropping it into some sample code, but not the 'onclick' action yet: <html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="initial-scale=1,maximum-scale=1,user-scalable=no"
/>
<title>
Intro to PopupTemplate | Sample | ArcGIS Maps SDK for JavaScript 4.27
</title>
<style>
html,
body,
#viewDiv {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
</style>
<link
rel="stylesheet"
href="https://js.arcgis.com/4.27/esri/themes/light/main.css"
/>
<script src="https://js.arcgis.com/4.27/"></script>
<script>
require([
"esri/Map",
"esri/layers/FeatureLayer",
"esri/views/MapView",
"esri/widgets/Legend"
], (Map, FeatureLayer, MapView, Legend) => {
// Create the map
const map = new Map({
basemap: "gray-vector"
});
// Create the MapView
const view = new MapView({
container: "viewDiv",
map: map,
center: [-73.95, 40.702],
zoom: 10
});
view.ui.add(new Legend({ view: view }), "bottom-left");
/*************************************************************
* The PopupTemplate content is the text that appears inside the
* popup. {fieldName} can be used to reference the value of an
* attribute of the selected feature. HTML elements can be used
* to provide structure and styles within the content. The
* fieldInfos property is an array of objects (each object representing
* a field) that is use to format number fields and customize field
* aliases in the popup and legend.
**************************************************************/
const template = {
// autocasts as new PopupTemplate()
title: "{NAME} in {COUNTY}",
content: "<img src='https://images.freeimages.com/fic/images/icons/694/longhorn_r2/256/forward_button.png' height='50px;' onlick='javascript:countyAssessor(''{COUNTY}'')>Click for {COUNTY} County Assessor web page'>"
};
// Reference the popupTemplate instance in the
// popupTemplate property of FeatureLayer
const featureLayer = new FeatureLayer({
url: "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/ACS_Marital_Status_Boundaries/FeatureServer/2",
popupTemplate: template
});
map.add(featureLayer);
});
</script>
</head>
<body>
<div id="viewDiv"></div>
</body>
</html> I wonder if you can just use the Popup 'Actions' and change the icon to be what you want it to be: https://developers.arcgis.com/javascript/latest/api-reference/esri-PopupTemplate.html#actions
... View more
06-23-2023
09:13 AM
|
1
|
1
|
7006
|
POST
|
Right off the bat I'm just going to say that 200 layers is waaayyyy too many layers to be loading. Even if you used ArcGIS Pro and had the data stored locally, the time it took to display that many would seem slow. There's also a difference in having 200 simple layers, like just a couple of points/lines, versus 200 layers with a few hundred (more?) records each, but both scenarious will run slow in performance, especially when exacerbated by being online. Some things to think about in trying to speed things up: 1. Make sure all of the data is in Web Mercator projection. That's what Online uses and even if you are using an internal Portal, if you use any online basemaps they will still be using Web Mecator. 2. Reduce the fields to only what is necessary. Having a large number of fields will not help anything. 3. Make sure the spatial indexes are there and up to date. You can update them for Online featureclasses by going into the Layer's settings. 4. Use scale dependency settings to make sure each layer is only showing at the scale(s) it is relevant to view at. In the past, I had developed an application that needed some large layers, and we only showed 1 or 2 layers upon open, and slowly added them to the map (not too slow) while the user started moving around. This was long before the current ArcGIS Online usage, but if you load an ArcGIS Online map with the data already loaded it might already have some of the performance fine tuned. Still, my recommendation is to figure out how to combine data layers, put your data on different maps, or re-evaluate why you need 200 layers. Maybe you could create one map with a few layers, then another map with some other layers, and then move between the maps when selected. Just a thought.
... View more
06-19-2023
09:05 AM
|
2
|
1
|
1662
|
POST
|
Not sure, maybe you need to add your HTML to Content instead of Description? I added a green button to this sample popup code from Esri: <html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>Popup actions | Sample | ArcGIS Maps SDK for JavaScript 4.27</title>
<style>
html,
body,
#viewDiv {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
</style>
<link rel="stylesheet" href="https://js.arcgis.com/4.27/esri/themes/light/main.css" />
<script src="https://js.arcgis.com/4.27/"></script>
<script>
require([
"esri/Map",
"esri/layers/FeatureLayer",
"esri/views/MapView",
"esri/geometry/geometryEngine",
"esri/core/reactiveUtils"
], (Map, FeatureLayer, MapView, geometryEngine, reactiveUtils) => {
// Create the Map
const map = new Map({
basemap: "gray-vector"
});
// Create the MapView
const view = new MapView({
container: "viewDiv",
map: map,
center: [-117.08, 34.1],
zoom: 11
});
/*************************************************************
* The PopupTemplate content is the text that appears inside the
* popup. Bracketed {fieldName} can be used to reference the value
* of an attribute of the selected feature. HTML elements can be
* used to provide structure and styles within the content.
**************************************************************/
// Add this action to the popup so it is always available in this view
const measureThisAction = {
title: "Measure Length",
id: "measure-this",
image: "https://developers.arcgis.com/javascript/latest//sample-code/popup-actions/live/Measure_Distance16.png"
};
const template = {
// autocasts as new PopupTemplate()
title: "Trail run",
content: "<div>{name}</div><div class='popup-content'><img src='https://w7.pngwing.com/pngs/714/342/png-transparent-computer-icons-button-green-green-3d-computer-graphics-grass-color-thumbnail.png' height='20px' alt='image description'></div>",
actions: [measureThisAction]
};
const featureLayer = new FeatureLayer({
url: "https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/TrailRuns/FeatureServer/0",
popupTemplate: template
});
map.add(featureLayer);
// Execute each time the "Measure Length" is clicked
function measureThis() {
const geom = view.popup.selectedFeature.geometry;
const initDistance = geometryEngine.geodesicLength(geom, "miles");
const distance = parseFloat(Math.round(initDistance * 100) / 100).toFixed(2);
view.popup.content =
view.popup.selectedFeature.attributes.name +
"<div style='background-color:DarkGray;color:white'>" +
distance +
" miles.</div>";
}
// Event handler that fires each time an action is clicked.
reactiveUtils.on(
() => view.popup,
"trigger-action",
(event) => {
// Execute the measureThis() function if the measure-this action is clicked
if (event.action.id === "measure-this") {
measureThis();
}
}
);
});
</script>
</head>
<body>
<div id="viewDiv"></div>
</body>
</html>
... View more
06-19-2023
08:45 AM
|
0
|
0
|
640
|
POST
|
Just checked in ArcMap 10.8.2 and the functionality was the same. Added an Excel table using 'Excel to Table'. Righ clicked on the table in the TOC and ran 'Display X Y Data', using the right lat/long fields and making sure to set the projection to Geographic/World/WGS84 (WKID:4326). Then you can export it to a shapfile (if desired) by righ clicking the new point featurclass, selecting 'Data' and then 'Export Data...'. It'll try to export the featurclass to a file geodatabse, but if you select the file (browse) icon you can specify the output is a shapefile.
... View more
05-03-2023
09:02 AM
|
0
|
0
|
3540
|
POST
|
Whew, it's seems like forever since I've opened ArcMap, but here's how I'd handle what you are talking about using ArcGIS Pro, and I would look for the similar functions in ArcMap. 1. Add CSV to the map, right click on it and select 'Display X Y Data', and select the easting (X) and northing (Y) fields. A new featureclass (FC) layer is added to your map and you can export this to a shapefile or file GDB if you want. 2. To 'display' this new point FC with a polygon FC, you just add them both to a map. If you are trying to get the values from one to the other then yes, it's the Spatial Join command. You are basically joining them based on their XY, so if you want the values from the polygon into the points, you set the point as your tartget FC and the polygons as your join FC and decide how to handle the fields.
... View more
05-03-2023
08:51 AM
|
0
|
1
|
3541
|
POST
|
If by 'login', you mean get access to the REST data on the server, then yes. If you can generate a token for a service then you can use it in the URL to access the data. For services that aren't protected, you don't need to pass the Token. Most servers are configured to not allow you to browse the REST services, so you pretty much need to know what you are looking for, but if you know the URL, and if it's protected and you have the credentials to generate a token, then you can just add the token to the URL request to the REST endpoint. For most Javascript applications that need to access protected data, setting up a proxy class to handle the token (if needed) helps.
... View more
04-18-2023
07:53 AM
|
0
|
0
|
454
|
Title | Kudos | Posted |
---|---|---|
1 | 11-01-2022 02:02 PM | |
1 | 02-28-2024 01:40 PM | |
2 | 02-27-2024 01:13 PM | |
1 | 10-18-2022 11:31 AM | |
1 | 06-23-2023 09:13 AM |
Online Status |
Offline
|
Date Last Visited |
05-28-2025
09:15 AM
|