def runTool(outFile, SQLDbase, inPointsLayer, inLocUniqueID, day, start_time,
            end_time, BufferSize, BufferUnits, DepOrArrChoice, username,
            password):
    def runOD(Points, Stops):
        # Call the OD Cost Matrix service for this set of chunks
        result = ODservice.GenerateOriginDestinationCostMatrix(
            Points,
            Stops,
            TravelMode,
            Distance_Units=BufferUnits,
            Cutoff=BufferSize,
            Origin_Destination_Line_Shape=PathShape)

        # Check the status of the result object every 0.5 seconds
        # until it has a value of 4(succeeded) or greater
        while result.status < 4:
            time.sleep(0.5)

        # Print any warning or error messages returned from the tool
        result_severity = result.maxSeverity
        if result_severity == 2:
            errors = result.getMessages(2)
            if "No solution found." in errors:
                # No destinations were found for the origins, which probably just means they were too far away.
                pass
            else:
                arcpy.AddError("An error occured when running the tool")
                arcpy.AddError(result.getMessages(2))
                raise BBB_SharedFunctions.CustomError
        elif result_severity == 1:
            arcpy.AddWarning("Warnings were returned when running the tool")
            arcpy.AddWarning(result.getMessages(1))

        # Get the resulting OD Lines and store the stops that are reachable from points.
        if result_severity != 2:
            linesSubLayer = result.getOutput(1)
            with arcpy.da.SearchCursor(
                    linesSubLayer,
                ["OriginOID", "DestinationOID"]) as ODCursor:
                for row in ODCursor:
                    UID = pointsOIDdict[row[0]]
                    SID = stopOIDdict[row[1]]
                    PointsAndStops.setdefault(str(UID), []).append(str(SID))

    try:
        # Source FC names are not prepended to field names.
        arcpy.env.qualifiedFieldNames = False
        # It's okay to overwrite in-memory stuff.
        OverwriteOutput = arcpy.env.overwriteOutput  # Get the orignal value so we can reset it.
        arcpy.env.overwriteOutput = True

        BBB_SharedFunctions.CheckArcVersion(min_version_pro="1.2")
        BBB_SharedFunctions.ConnectToSQLDatabase(SQLDbase)

        Specific, day = BBB_SharedFunctions.CheckSpecificDate(day)
        start_sec, end_sec = BBB_SharedFunctions.ConvertTimeWindowToSeconds(
            start_time, end_time)

        # Distance between stops and points
        BufferSize_padded = BufferSize + (.2 * BufferSize)
        BufferLinearUnit = str(BufferSize_padded) + " " + BufferUnits

        # Will we calculate the max wait time?
        CalcWaitTime = True

        # Output file designated by user
        outDir = os.path.dirname(outFile)
        outFilename = os.path.basename(outFile)
        ispgdb = "esriDataSourcesGDB.AccessWorkspaceFactory" in arcpy.Describe(
            outDir).workspaceFactoryProgID

        inLocUniqueID = BBB_SharedFunctions.HandleOIDUniqueID(
            inPointsLayer, inLocUniqueID)

        # ----- Prepare OD service -----
        try:
            arcpy.AddMessage(
                "Obtaining credentials for and information about OD Cost Matrix service..."
            )

            # Hard-wired OD variables
            TravelMode = "Walking Distance"
            PathShape = "None"

            OD_service_name = "World/OriginDestinationCostMatrix"
            Utility_service_name = "World/Utilities"
            # Get the credentials from the signed in user and import the service
            if username and password:
                ODservice = BBB_SharedFunctions.import_AGOLservice(
                    OD_service_name, username=username, password=password)
                Utilityservice = BBB_SharedFunctions.import_AGOLservice(
                    Utility_service_name, username=username, password=password)
            else:
                credentials = arcpy.GetSigninToken()
                if not credentials:
                    arcpy.AddError(
                        "Please sign into ArcGIS Online or pass a username and password to the tool."
                    )
                    raise BBB_SharedFunctions.CustomError
                token = credentials["token"]
                referer = credentials["referer"]
                ODservice = BBB_SharedFunctions.import_AGOLservice(
                    OD_service_name, token=token, referer=referer)
                Utilityservice = BBB_SharedFunctions.import_AGOLservice(
                    Utility_service_name, token=token, referer=referer)

            # Get the service limits from the OD service (how many origins and destinations allowed)
            utilresult = Utilityservice.GetToolInfo(
                "asyncODCostMatrix", "GenerateOriginDestinationCostMatrix")
            utilresultstring = utilresult.getOutput(0)
            utilresultjson = json.loads(utilresultstring)
            origin_limit = int(
                utilresultjson['serviceLimits']['maximumDestinations'])
            destination_limit = int(
                utilresultjson['serviceLimits']['maximumOrigins'])

        except:
            arcpy.AddError(
                "Failed to obtain credentials for and information about OD Cost Matrix service."
            )
            raise

        # ----- Create a feature class of stops ------
        try:
            arcpy.AddMessage("Getting GTFS stops...")
            tempstopsname = "Temp_Stops"
            StopsLayer, StopList = BBB_SharedFunctions.MakeStopsFeatureClass(
                os.path.join(outDir, tempstopsname))

            # Select only the stops within a reasonable distance of points to reduce problem size
            arcpy.management.MakeFeatureLayer(StopsLayer, "StopsToRemove")
            arcpy.management.SelectLayerByLocation(
                "StopsToRemove",
                "WITHIN_A_DISTANCE_GEODESIC",
                inPointsLayer,
                BufferLinearUnit,
                invert_spatial_relationship="INVERT")
            arcpy.management.DeleteRows("StopsToRemove")
            arcpy.management.Delete("StopsToRemove")

            # Make Feature Layer of stops to use later
            arcpy.management.MakeFeatureLayer(StopsLayer, "StopsLayer")
            stopsOID = arcpy.Describe("StopsLayer").OIDFieldName

        except:
            arcpy.AddError("Error creating feature class of GTFS stops.")
            raise

        # ----- Prepare input data -----
        try:
            arcpy.AddMessage("Preparing input points...")

            # Select only the points within a reasonable distance of stops to reduce problem size
            temppointsname = outFilename + "_Temp"
            relevantPoints = os.path.join(outDir, temppointsname)
            arcpy.management.MakeFeatureLayer(inPointsLayer, "PointsToKeep")
            arcpy.management.SelectLayerByLocation(
                "PointsToKeep", "WITHIN_A_DISTANCE_GEODESIC", StopsLayer,
                BufferLinearUnit)
            num_points = int(
                arcpy.management.GetCount("PointsToKeep").getOutput(0))

            # If the number of points is large, sort them spatially for smart chunking
            if num_points > origin_limit:
                shapeFieldName = arcpy.Describe("PointsToKeep").shapeFieldName
                arcpy.management.Sort("PointsToKeep", relevantPoints,
                                      shapeFieldName, "PEANO")
            # Otherwise, just copy them.
            else:
                arcpy.management.CopyFeatures("PointsToKeep", relevantPoints)
            arcpy.management.Delete("PointsToKeep")

            # Store OIDs in a dictionary for later joining
            pointsOIDdict = {}  # {OID: inLocUniqueID}
            with arcpy.da.SearchCursor(relevantPoints,
                                       ["OID@", inLocUniqueID]) as cur:
                for row in cur:
                    pointsOIDdict[row[0]] = row[1]
            relevantpointsOID = arcpy.Describe(relevantPoints).OIDFieldName

        except:
            arcpy.AddError("Error preparing input points for analysis.")
            raise

        #----- Create OD Matrix between stops and user's points -----
        try:
            arcpy.AddMessage("Creating OD matrix between points and stops...")
            arcpy.AddMessage(
                "(This step could take a while for large datasets or buffer sizes.)"
            )

            global PointsAndStops
            # PointsAndStops = {LocID: [stop_1, stop_2, ...]}
            PointsAndStops = {}

            # Chunk the points to fit the service limits and loop through chunks
            points_numchunks = int(math.ceil(float(num_points) / origin_limit))
            points_chunkstart = 0
            points_chunkend = origin_limit
            current_chunk = 0
            for x in range(0, points_numchunks):
                current_chunk += 1
                arcpy.AddMessage("Handling input points chunk %i of %i" %
                                 (current_chunk, points_numchunks))

                # Select only the points belonging to this chunk
                points_chunk = sorted(
                    pointsOIDdict.keys())[points_chunkstart:points_chunkend]
                points_chunkstart = points_chunkend
                points_chunkend = points_chunkstart + origin_limit
                if ispgdb:
                    points_selection_query = '[{0}] IN ({1})'.format(
                        relevantpointsOID, ','.join(map(str, points_chunk)))
                else:
                    points_selection_query = '"{0}" IN ({1})'.format(
                        relevantpointsOID, ','.join(map(str, points_chunk)))
                arcpy.MakeFeatureLayer_management(relevantPoints,
                                                  "PointsLayer",
                                                  points_selection_query)

                # Select only the stops within the safe buffer of these points
                arcpy.management.SelectLayerByLocation(
                    "StopsLayer", "WITHIN_A_DISTANCE_GEODESIC", "PointsLayer",
                    BufferLinearUnit)
                num_stops = int(
                    arcpy.GetCount_management("StopsLayer").getOutput(0))
                stopOIDdict = {}  # {OID: stop_id}
                with arcpy.da.SearchCursor("StopsLayer",
                                           ["OID@", "stop_id"]) as cur:
                    for row in cur:
                        stopOIDdict[row[0]] = row[1]

                # If the number of stops in range exceeds the destination limit, we have to chunk these as well.
                if num_stops > destination_limit:
                    stops_numchunks = int(
                        math.ceil(float(num_stops) / destination_limit))
                    stops_chunkstart = 0
                    stops_chunkend = destination_limit
                    for x in range(0, stops_numchunks):
                        stops_chunk = sorted(stopOIDdict.keys()
                                             )[stops_chunkstart:stops_chunkend]
                        stops_chunkstart = stops_chunkend
                        stops_chunkend = stops_chunkstart + destination_limit
                        if ispgdb:
                            stops_selection_query = '[{0}] IN ({1})'.format(
                                stopsOID, ','.join(map(str, stops_chunk)))
                        else:
                            stops_selection_query = '"{0}" IN ({1})'.format(
                                stopsOID, ','.join(map(str, stops_chunk)))
                        arcpy.MakeFeatureLayer_management(
                            "StopsLayer", "StopsLayer_Chunk",
                            stops_selection_query)
                        runOD("PointsLayer", "StopsLayer_Chunk")
                    arcpy.management.Delete("StopsLayer_Chunk")
                # Otherwise, just run them all.
                else:
                    runOD("PointsLayer", "StopsLayer")

            # Clean up
            arcpy.management.Delete("StopsLayer")
            arcpy.management.Delete("PointsLayer")
            arcpy.management.Delete(StopsLayer)
            arcpy.management.Delete(relevantPoints)

        except:
            arcpy.AddError(
                "Error creating OD matrix between stops and input points.")
            raise

        #----- Query the GTFS data to count the trips at each stop -----
        try:
            arcpy.AddMessage(
                "Calculating the number of transit trips available during the time window..."
            )

            # Get a dictionary of stop times in our time window {stop_id: [[trip_id, stop_time]]}
            stoptimedict = BBB_SharedFunctions.CountTripsAtStops(
                day, start_sec, end_sec,
                BBB_SharedFunctions.CleanUpDepOrArr(DepOrArrChoice), Specific)

        except:
            arcpy.AddError(
                "Error calculating the number of transit trips available during the time window."
            )
            raise

        # ----- Generate output data -----
        try:
            arcpy.AddMessage("Writing output data...")

            arcpy.management.CopyFeatures(inPointsLayer, outFile)
            # Add a field to the output file for number of trips and num trips / hour.
            arcpy.management.AddField(outFile, "NumTrips", "SHORT")
            arcpy.management.AddField(outFile, "NumTripsPerHr", "DOUBLE")
            arcpy.management.AddField(outFile, "NumStopsInRange", "SHORT")
            arcpy.management.AddField(outFile, "MaxWaitTime", "SHORT")

            with arcpy.da.UpdateCursor(outFile, [
                    inLocUniqueID, "NumTrips", "NumTripsPerHr",
                    "NumStopsInRange", "MaxWaitTime"
            ]) as ucursor:
                for row in ucursor:
                    try:
                        ImportantStops = PointsAndStops[str(row[0])]
                    except KeyError:
                        # This point had no stops in range
                        ImportantStops = []
                    NumTrips, NumTripsPerHr, NumStopsInRange, MaxWaitTime =\
                                    BBB_SharedFunctions.RetrieveStatsForSetOfStops(
                                        ImportantStops, stoptimedict, CalcWaitTime,
                                        start_sec, end_sec)
                    row[1] = NumTrips
                    row[2] = NumTripsPerHr
                    row[3] = NumStopsInRange
                    row[4] = MaxWaitTime
                    ucursor.updateRow(row)

        except:
            arcpy.AddError("Error writing output.")
            raise

        arcpy.AddMessage("Done!")
        arcpy.AddMessage("Output files written:")
        arcpy.AddMessage("- " + outFile)

    except BBB_SharedFunctions.CustomError:
        arcpy.AddError("Error counting transit trips at input locations.")
        pass

    except:
        arcpy.AddError("Error counting transit trips at input locations.")
        raise

    finally:
        # Reset overwriteOutput to what it was originally.
        arcpy.env.overwriteOutput = OverwriteOutput
def runTool(outFile, SQLDbase, inPointsLayer, inLocUniqueID, day, start_time,
            end_time, inNetworkDataset, imp, BufferSize, restrictions,
            DepOrArrChoice):
    try:
        # Source FC names are not prepended to field names.
        arcpy.env.qualifiedFieldNames = False
        # It's okay to overwrite in-memory stuff.
        OverwriteOutput = arcpy.env.overwriteOutput  # Get the orignal value so we can reset it.
        arcpy.env.overwriteOutput = True

        BBB_SharedFunctions.CheckArcVersion(min_version_pro="1.2")
        ProductName = BBB_SharedFunctions.ProductName
        BBB_SharedFunctions.CheckWorkspace()
        BBB_SharedFunctions.CheckOutNALicense()

        BBB_SharedFunctions.ConnectToSQLDatabase(SQLDbase)

        Specific, day = BBB_SharedFunctions.CheckSpecificDate(day)
        start_sec, end_sec = BBB_SharedFunctions.ConvertTimeWindowToSeconds(
            start_time, end_time)

        # Will we calculate the max wait time?
        CalcWaitTime = True

        impedanceAttribute = BBB_SharedFunctions.CleanUpImpedance(imp)

        # Hard-wired OD variables
        ExcludeRestricted = "EXCLUDE"
        PathShape = "NO_LINES"
        accumulate = ""
        uturns = "ALLOW_UTURNS"
        hierarchy = "NO_HIERARCHY"

        # Output file designated by user
        outDir = os.path.dirname(outFile)
        outFilename = os.path.basename(outFile)

        inLocUniqueID = BBB_SharedFunctions.HandleOIDUniqueID(
            inPointsLayer, inLocUniqueID)
        inLocUniqueID_qualified = inLocUniqueID + "_Input"

        arcpy.AddMessage("Run set up successfully.")

        # ----- Create a feature class of stops ------
        try:
            arcpy.AddMessage("Getting GTFS stops...")
            tempstopsname = "Temp_Stops"
            if ".shp" in outFilename:
                tempstopsname += ".shp"
            StopsLayer, StopList = BBB_SharedFunctions.MakeStopsFeatureClass(
                os.path.join(outDir, tempstopsname))
        except:
            arcpy.AddError("Error creating feature class of GTFS stops.")
            raise

        #----- Create OD Matrix between stops and user's points -----
        try:
            arcpy.AddMessage("Creating OD matrix between points and stops...")
            arcpy.AddMessage(
                "(This step could take a while for large datasets or buffer sizes.)"
            )

            # Name to refer to OD matrix layer
            outNALayer_OD = "ODMatrix"

            # ODLayer is the NA Layer object returned by getOutput(0)
            ODLayer = arcpy.na.MakeODCostMatrixLayer(
                inNetworkDataset, outNALayer_OD, impedanceAttribute,
                BufferSize, "", accumulate, uturns, restrictions, hierarchy,
                "", PathShape).getOutput(0)

            # To refer to the OD sublayers, get the sublayer names.  This is essential for localization.
            naSubLayerNames = arcpy.na.GetNAClassNames(ODLayer)
            points = naSubLayerNames["Origins"]
            stops = naSubLayerNames["Destinations"]

            # Add a field for stop_id as a unique identifier for stops.
            arcpy.na.AddFieldToAnalysisLayer(outNALayer_OD, stops, "stop_id",
                                             "TEXT")
            # Specify the field mappings for the stop_id field.
            fieldMappingStops = arcpy.na.NAClassFieldMappings(ODLayer, stops)
            fieldMappingStops["Name"].mappedFieldName = "stop_id"
            fieldMappingStops["stop_id"].mappedFieldName = "stop_id"
            # Add the GTFS stops as locations for the analysis.
            arcpy.na.AddLocations(outNALayer_OD, stops, StopsLayer,
                                  fieldMappingStops, "500 meters", "", "", "",
                                  "", "", "", ExcludeRestricted)
            # Clear out the memory because we don't need this anymore.
            arcpy.management.Delete(StopsLayer)

            # Add a field for unique identifier for points.
            arcpy.na.AddFieldToAnalysisLayer(outNALayer_OD, points,
                                             inLocUniqueID_qualified, "TEXT")
            # Specify the field mappings for the unique id field.
            fieldMappingPoints = arcpy.na.NAClassFieldMappings(ODLayer, points)
            fieldMappingPoints["Name"].mappedFieldName = inLocUniqueID
            fieldMappingPoints[
                inLocUniqueID_qualified].mappedFieldName = inLocUniqueID
            # Add the input points as locations for the analysis.
            arcpy.na.AddLocations(outNALayer_OD, points, inPointsLayer,
                                  fieldMappingPoints, "500 meters", "", "", "",
                                  "", "", "", ExcludeRestricted)

            # Solve the OD matrix.
            try:
                arcpy.na.Solve(outNALayer_OD)
            except:
                errs = arcpy.GetMessages(2)
                if "No solution found" in errs:
                    impunits = imp.split(" (Units: ")[1].split(")")[0]
                    arcpy.AddError(
                        "No transit stops were found within a %s %s walk of any of your input points.  \
Consequently, there is no transit service available to your input points, so no output will be generated."
                        % (str(BufferSize), impunits))
                else:
                    arcpy.AddError(
                        "Failed to calculate travel time or distance between transit stops and input points.  OD Cost Matrix error messages:"
                    )
                    arcpy.AddError(errs)
                raise BBB_SharedFunctions.CustomError

            # Make layer objects for each sublayer we care about.
            if ProductName == 'ArcGISPro':
                naSubLayerNames = arcpy.na.GetNAClassNames(ODLayer)
                subLayerDict = dict(
                    (lyr.name, lyr) for lyr in ODLayer.listLayers())
                subLayers = {}
                for subL in naSubLayerNames:
                    subLayers[subL] = subLayerDict[naSubLayerNames[subL]]
            else:
                subLayers = dict(
                    (lyr.datasetName, lyr)
                    for lyr in arcpy.mapping.ListLayers(ODLayer)[1:])
            linesSubLayer = subLayers["ODLines"]
            pointsSubLayer = subLayers["Origins"]
            stopsSubLayer = subLayers["Destinations"]

            # Get the OID fields, just to be thorough
            desc1 = arcpy.Describe(pointsSubLayer)
            points_OID = desc1.OIDFieldName
            desc2 = arcpy.Describe(stopsSubLayer)
            stops_OID = desc2.OIDFieldName

            # Join polygons layer with input facilities to port over the stop_id
            arcpy.management.JoinField(linesSubLayer, "OriginID",
                                       pointsSubLayer, points_OID,
                                       [inLocUniqueID_qualified])
            arcpy.management.JoinField(linesSubLayer, "DestinationID",
                                       stopsSubLayer, stops_OID, ["stop_id"])

            # Use searchcursor on lines to find the stops that are reachable from points.
            global PointsAndStops
            # PointsAndStops = {LocID: [stop_1, stop_2, ...]}
            PointsAndStops = {}
            ODCursor = arcpy.da.SearchCursor(
                linesSubLayer, [inLocUniqueID_qualified, "stop_id"])
            for row in ODCursor:
                PointsAndStops.setdefault(str(row[0]), []).append(str(row[1]))
            del ODCursor

        except:
            arcpy.AddError(
                "Error creating OD matrix between stops and input points.")
            raise

        #----- Query the GTFS data to count the trips at each stop -----
        try:
            arcpy.AddMessage(
                "Calculating the number of transit trips available during the time window..."
            )

            # Get a dictionary of stop times in our time window {stop_id: [[trip_id, stop_time]]}
            stoptimedict = BBB_SharedFunctions.CountTripsAtStops(
                day, start_sec, end_sec,
                BBB_SharedFunctions.CleanUpDepOrArr(DepOrArrChoice), Specific)

        except:
            arcpy.AddError(
                "Error calculating the number of transit trips available during the time window."
            )
            raise

        # ----- Generate output data -----
        try:
            arcpy.AddMessage("Writing output data...")

            arcpy.management.CopyFeatures(inPointsLayer, outFile)
            # Add a field to the output file for number of trips and num trips / hour.
            if ".shp" in outFilename:
                arcpy.management.AddField(outFile, "NumTrips", "SHORT")
                arcpy.management.AddField(outFile, "TripsPerHr", "DOUBLE")
                arcpy.management.AddField(outFile, "NumStops", "SHORT")
                arcpy.management.AddField(outFile, "MaxWaitTm", "SHORT")
            else:
                arcpy.management.AddField(outFile, "NumTrips", "SHORT")
                arcpy.management.AddField(outFile, "NumTripsPerHr", "DOUBLE")
                arcpy.management.AddField(outFile, "NumStopsInRange", "SHORT")
                arcpy.management.AddField(outFile, "MaxWaitTime", "SHORT")

            if ".shp" in outFilename:
                ucursor = arcpy.da.UpdateCursor(outFile, [
                    inLocUniqueID[0:10], "NumTrips", "TripsPerHr", "NumStops",
                    "MaxWaitTm"
                ])
            else:
                ucursor = arcpy.da.UpdateCursor(outFile, [
                    inLocUniqueID, "NumTrips", "NumTripsPerHr",
                    "NumStopsInRange", "MaxWaitTime"
                ])
            for row in ucursor:
                try:
                    ImportantStops = PointsAndStops[str(row[0])]
                except KeyError:
                    # This point had no stops in range
                    ImportantStops = []
                NumTrips, NumTripsPerHr, NumStopsInRange, MaxWaitTime =\
                                BBB_SharedFunctions.RetrieveStatsForSetOfStops(
                                    ImportantStops, stoptimedict, CalcWaitTime,
                                    start_sec, end_sec)
                row[1] = NumTrips
                row[2] = NumTripsPerHr
                row[3] = NumStopsInRange
                if ".shp" in outFilename and MaxWaitTime == None:
                    row[4] = -1
                else:
                    row[4] = MaxWaitTime
                ucursor.updateRow(row)

        except:
            arcpy.AddError("Error writing output.")
            raise

        arcpy.AddMessage("Done!")
        arcpy.AddMessage("Output files written:")
        arcpy.AddMessage("- " + outFile)

    except BBB_SharedFunctions.CustomError:
        arcpy.AddError("Error counting transit trips at input locations.")
        pass

    except:
        arcpy.AddError("Error counting transit trips at input locations.")
        raise

    finally:
        # Reset overwriteOutput to what it was originally.
        arcpy.env.overwriteOutput = OverwriteOutput