From 7f9d32424a79c5c3066772235cab94ea8b9aa073 Mon Sep 17 00:00:00 2001 From: Nick Heppler Date: Thu, 27 Mar 2025 07:54:15 -0400 Subject: [PATCH 1/5] Add debug logging messages. --- app.py | 238 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 173 insertions(+), 65 deletions(-) diff --git a/app.py b/app.py index 4729c21..14a1e0d 100644 --- a/app.py +++ b/app.py @@ -8,20 +8,37 @@ import argparse import urllib.parse from dotenv import load_dotenv +# Load environment variables from .env file +load_dotenv("753DataSync.env") + # Configuration BASE_URL = "{}/{}/{}" +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() # Ensure it's uppercase to match logging levels # Setup logging logger = logging.getLogger() -logger.setLevel(logging.INFO) + +# Dynamically set the log level for the logger +if log_level == 'DEBUG': + logger.setLevel(logging.DEBUG) +elif log_level == 'INFO': + logger.setLevel(logging.INFO) +elif log_level == 'WARNING': + logger.setLevel(logging.WARNING) +elif log_level == 'ERROR': + logger.setLevel(logging.ERROR) +elif log_level == 'CRITICAL': + logger.setLevel(logging.CRITICAL) +else: + logger.setLevel(logging.INFO) # Default to INFO if the level is invalid # File handler file_handler = logging.FileHandler('753DataSync.log') -file_handler.setLevel(logging.INFO) +file_handler.setLevel(getattr(logging, log_level)) # Set file handler level dynamically # Stream handler (console output) stream_handler = logging.StreamHandler(sys.stdout) -stream_handler.setLevel(logging.INFO) +stream_handler.setLevel(getattr(logging, log_level)) # Set stream handler level dynamically # Log format formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') @@ -37,23 +54,30 @@ def fetch_data(api_url, page_number, results_per_page): url = BASE_URL.format(api_url, page_number, results_per_page) try: - logger.info(f"Making request to: {url}") + logger.info(f"Making request to: {url} with page_number={page_number} and results_per_page={results_per_page}") response = requests.get(url) # Check for HTTP errors response.raise_for_status() + # Success log + logger.info(f"Successfully fetched data from {url}. Status code: {response.status_code}.") + + # Debug log with additional response details + logger.debug(f"GET request to {url} completed with status code {response.status_code}. " + f"Response time: {response.elapsed.total_seconds()} seconds.") + # Return JSON data return response.json() except requests.exceptions.HTTPError as http_err: - logger.error(f"HTTP error occurred: {http_err}") + logger.error(f"HTTP error occurred while fetching data from {url}: {http_err}") sys.exit(1) except requests.exceptions.RequestException as req_err: - logger.error(f"Request error occurred: {req_err}") + logger.error(f"Request error occurred while fetching data from {url}: {req_err}") sys.exit(1) except Exception as err: - logger.error(f"An unexpected error occurred: {err}") + logger.exception(f"An unexpected error occurred while fetching data from {url}: {err}") sys.exit(1) def save_json(data, filename): @@ -62,15 +86,22 @@ def save_json(data, filename): # Ensure directory exists if not os.path.exists('data'): os.makedirs('data') - + logger.info(f"Directory 'data' created.") + # Save data to file with open(filename, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=4) - logger.info(f"Data saved to {filename}") + logger.info(f"Data successfully saved to {filename}") + except OSError as e: + logger.error(f"OS error occurred while saving JSON data to {filename}: {e}") + sys.exit(1) + except IOError as e: + logger.error(f"I/O error occurred while saving JSON data to {filename}: {e}") + sys.exit(1) except Exception as e: - logger.error(f"Error saving JSON data: {e}") + logger.error(f"Unexpected error occurred while saving JSON data to {filename}: {e}") sys.exit(1) def parse_arguments(): @@ -96,14 +127,36 @@ def generate_token(username, password, url="https://www.arcgis.com/sharing/rest/ 'expiration': '120' } headers = {} + try: + logger.info(f"Generating token for username '{username}' using URL: {url}") response = requests.post(url, headers=headers, data=payload) + + # Log the request status and response time + logger.debug(f"POST request to {url} completed with status code {response.status_code}. " + f"Response time: {response.elapsed.total_seconds()} seconds.") + response.raise_for_status() # Raise an error for bad status codes - token = response.json()['token'] - logger.info("Token generated successfully.") + + # Extract token from the response + token = response.json().get('token') + + if token: + logger.info("Token generated successfully.") + else: + logger.error("Token not found in the response.") + sys.exit(1) + return token + except requests.exceptions.RequestException as e: - logger.error(f"Error generating token: {e}") + logger.error(f"Error generating token for username '{username}': {e}") + sys.exit(1) + except KeyError as e: + logger.error(f"Error extracting token from the response: Missing key {e}") + sys.exit(1) + except Exception as e: + logger.exception(f"Unexpected error generating token for username '{username}': {e}") sys.exit(1) def truncate(token, hostname, instance, fs, layer, secure=True): @@ -113,10 +166,17 @@ def truncate(token, hostname, instance, fs, layer, secure=True): url = f"{protocol}{hostname}/{instance}/arcgis/rest/admin/services/{fs}/FeatureServer/{layer}/truncate?token={token}&async=true&f=json" try: - # Attempt the POST request logging.info(f"Attempting to truncate layer {layer} on {hostname}...") + + # Debug logging for the URL being used + logging.debug(f"Truncate URL: {url}") + response = requests.post(url, timeout=30) + # Log response time + logging.debug(f"POST request to {url} completed with status code {response.status_code}. " + f"Response time: {response.elapsed.total_seconds()} seconds.") + # Check for HTTP errors response.raise_for_status() # Raise an exception for HTTP errors (4xx, 5xx) @@ -124,28 +184,30 @@ def truncate(token, hostname, instance, fs, layer, secure=True): if response.status_code == 200: result = response.json() if 'error' in result: - logging.error(f"Error truncating layer: {result['error']}") + logging.error(f"Error truncating layer {layer}: {result['error']}") return None logging.info(f"Successfully truncated layer: {protocol}{hostname}/{instance}/arcgis/rest/admin/services/{fs}/FeatureServer/{layer}.") return result else: - logging.error(f"Unexpected response: {response.status_code} - {response.text}") + logging.error(f"Unexpected response for layer {layer}: {response.status_code} - {response.text}") return None + except requests.exceptions.Timeout as e: + logging.error(f"Request timed out while truncating layer {layer}: {e}") + return None except requests.exceptions.RequestException as e: - # Catch network-related errors, timeouts, etc. - logging.error(f"Request failed: {e}") + logging.error(f"Request failed while truncating layer {layer}: {e}") return None except Exception as e: - # Catch any other unexpected errors - logging.error(f"An unexpected error occurred: {e}") + logging.error(f"An unexpected error occurred while truncating layer {layer}: {e}") return None def add_features(token, hostname, instance, fs, layer, aggregated_data, secure=True): """Add features to a feature service.""" protocol = 'https://' if secure else 'http://' url = f"{protocol}{hostname}/{instance}/arcgis/rest/services/{fs}/FeatureServer/{layer}/addFeatures?token={token}&rollbackOnFailure=true&f=json" - logger.info(f"Attempting to add features on {protocol}{hostname}/{instance}/arcgis/rest/services/{fs}/FeatureServer/{layer}...") + + logger.info(f"Attempting to add features to {protocol}{hostname}/{instance}/arcgis/rest/services/{fs}/FeatureServer/{layer}...") # Prepare features data as the payload features_json = json.dumps(aggregated_data) # Convert aggregated data to JSON string @@ -159,73 +221,119 @@ def add_features(token, hostname, instance, fs, layer, aggregated_data, secure=T } try: + # Log request details (but avoid logging sensitive data) + logger.debug(f"Request URL: {url}") + logger.debug(f"Payload size: {len(features_json)} characters") + response = requests.post(url, headers=headers, data=payload, timeout=180) + + # Log the response time and status code + logger.debug(f"POST request to {url} completed with status code {response.status_code}. " + f"Response time: {response.elapsed.total_seconds()} seconds.") + response.raise_for_status() # Raise an error for bad status codes + logger.info("Features added successfully.") + + # Log any successful response details + if response.status_code == 200: + logger.debug(f"Response JSON size: {len(response.text)} characters.") + return response.json() + + except requests.exceptions.Timeout as e: + logger.error(f"Request timed out while adding features: {e}") + return {'error': 'Request timed out'} + except requests.exceptions.RequestException as e: - logger.error(f"Request error: {e}") + logger.error(f"Request error occurred while adding features: {e}") return {'error': str(e)} + except json.JSONDecodeError as e: - logger.error(f"Error decoding JSON response: {e}") + logger.error(f"Error decoding JSON response while adding features: {e}") return {'error': 'Invalid JSON response'} + except Exception as e: + logger.error(f"An unexpected error occurred while adding features: {e}") + return {'error': str(e)} + def main(): """Main entry point for the script.""" - # Parse command-line arguments - results_per_page = parse_arguments() + try: + logger.info("Starting script execution.") - load_dotenv("753DataSync.env") - api_url = os.getenv('API_URL') + # Parse command-line arguments + results_per_page = parse_arguments() + logger.info(f"Parsed arguments: results_per_page={results_per_page}") - # Generate the token - username = os.getenv('AGOL_USER') - password = os.getenv('AGOL_PASSWORD') - token = generate_token(username, password) + # Load environment variables + logger.info("Loading environment variables.") + load_dotenv("753DataSync.env") + api_url = os.getenv('API_URL') + if not api_url: + logger.error("API_URL environment variable not found.") + return - # Set ArcGIS host details - hostname = os.getenv('HOSTNAME') - instance = os.getenv('INSTANCE') - fs = os.getenv('FS') - layer = os.getenv('LAYER') + # Generate the token + username = os.getenv('AGOL_USER') + password = os.getenv('AGOL_PASSWORD') + if not username or not password: + logger.error("Missing AGOL_USER or AGOL_PASSWORD in environment variables.") + return + token = generate_token(username, password) - # Truncate the layer before adding new features - truncate(token, hostname, instance, fs, layer) + # Set ArcGIS host details + hostname = os.getenv('HOSTNAME') + instance = os.getenv('INSTANCE') + fs = os.getenv('FS') + layer = os.getenv('LAYER') - all_data = [] - page_number = 1 + # Truncate the layer before adding new features + truncate(token, hostname, instance, fs, layer) - while True: - # Fetch data from the API - data = fetch_data(api_url, page_number, results_per_page) + all_data = [] + page_number = 1 - # Append features data to the aggregated list - all_data.extend(data) # Data is now a list of features + while True: + try: + # Fetch data from the API + data = fetch_data(api_url, page_number, results_per_page) - # Generate filename with timestamp for the individual page - timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - page_filename = f"data/enforcement_page_{page_number}_results_{results_per_page}_{timestamp}.json" - - # Save individual page data - save_json(data, page_filename) + # Append features data to the aggregated list + all_data.extend(data) - # Check if the number of records is less than the results_per_page, indicating last page - if len(data) < results_per_page: - logger.info("No more data to fetch, stopping pagination.") - break + # Generate filename with timestamp for the individual page + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + page_filename = f"data/enforcement_page_{page_number}_results_{results_per_page}_{timestamp}.json" + + # Save individual page data + save_json(data, page_filename) - page_number += 1 + # Check if the number of records is less than the results_per_page, indicating last page + if len(data) < results_per_page: + logger.info("No more data to fetch, stopping pagination.") + break - # Prepare aggregated data - aggregated_data = all_data # Just use the collected features directly + page_number += 1 + except Exception as e: + logger.error(f"Error fetching or saving data for page {page_number}: {e}", exc_info=True) + break - # Save aggregated data to a single JSON file - aggregated_filename = f"data/aggregated_enforcement_results_{timestamp}.json" - save_json(aggregated_data, aggregated_filename) + # Prepare aggregated data + aggregated_data = all_data # Just use the collected features directly - # Add the features to the feature layer - response = add_features(token, hostname, instance, fs, layer, aggregated_data) - logger.info(f"Add features response: {json.dumps(response, indent=2)}") + # Save aggregated data to a single JSON file + aggregated_filename = f"data/aggregated_enforcement_results_{timestamp}.json" + logger.info(f"Saving aggregated data to {aggregated_filename}.") + save_json(aggregated_data, aggregated_filename) + + # Add the features to the feature layer + response = add_features(token, hostname, instance, fs, layer, aggregated_data) + except Exception as e: + logger.error(f"An unexpected error occurred: {e}", exc_info=True) + return + finally: + logger.info("Script execution completed.") if __name__ == "__main__": - main() + main() \ No newline at end of file -- 2.45.2 From 01e0d16838734140be16f80148197523a785fad4 Mon Sep 17 00:00:00 2001 From: Nick Heppler Date: Tue, 1 Apr 2025 19:15:33 -0400 Subject: [PATCH 2/5] Only save the individual page files if the logging level is set to debug. --- app.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index 14a1e0d..69e176e 100644 --- a/app.py +++ b/app.py @@ -13,12 +13,12 @@ load_dotenv("753DataSync.env") # Configuration BASE_URL = "{}/{}/{}" -log_level = os.getenv('LOG_LEVEL', 'INFO').upper() # Ensure it's uppercase to match logging levels +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() # Setup logging logger = logging.getLogger() -# Dynamically set the log level for the logger +# Set the log level for the logger if log_level == 'DEBUG': logger.setLevel(logging.DEBUG) elif log_level == 'INFO': @@ -30,15 +30,15 @@ elif log_level == 'ERROR': elif log_level == 'CRITICAL': logger.setLevel(logging.CRITICAL) else: - logger.setLevel(logging.INFO) # Default to INFO if the level is invalid + logger.setLevel(logging.INFO) # File handler file_handler = logging.FileHandler('753DataSync.log') -file_handler.setLevel(getattr(logging, log_level)) # Set file handler level dynamically +file_handler.setLevel(getattr(logging, log_level)) # Stream handler (console output) stream_handler = logging.StreamHandler(sys.stdout) -stream_handler.setLevel(getattr(logging, log_level)) # Set stream handler level dynamically +stream_handler.setLevel(getattr(logging, log_level)) # Log format formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') @@ -302,12 +302,12 @@ def main(): # Append features data to the aggregated list all_data.extend(data) - # Generate filename with timestamp for the individual page timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") page_filename = f"data/enforcement_page_{page_number}_results_{results_per_page}_{timestamp}.json" # Save individual page data - save_json(data, page_filename) + if log_level == 'DEBUG': + save_json(data, page_filename) # Check if the number of records is less than the results_per_page, indicating last page if len(data) < results_per_page: -- 2.45.2 From da4ef4dc550d7c398538b640161f6505970b5449 Mon Sep 17 00:00:00 2001 From: Nick Heppler Date: Tue, 1 Apr 2025 19:19:30 -0400 Subject: [PATCH 3/5] Update README --- README.md | 65 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index a99519c..61ce81a 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ This script fetches enforcement data from an external API, truncates a specified feature layer in ArcGIS, and adds the fetched data as features to the layer. The script performs the following tasks: -1. **Truncate the specified layer** in ArcGIS to clear any previous features before adding new ones. -2. **Fetch data** from an API in paginated form. -3. **Save data** from each API response to individual JSON files. -4. **Aggregate all data** from all pages into one JSON file. -5. **Add the aggregated data** as features to an ArcGIS feature service. +- **Truncate** the specified layer in ArcGIS to clear any previous features before adding new ones. +- **Fetch** data from an API in paginated form. +- **Save** data from each API response to individual JSON files. +- **Aggregate** all data from all pages into one JSON file. +- **Add** the aggregated data as features to an ArcGIS feature service. ## Requirements @@ -15,17 +15,24 @@ This script fetches enforcement data from an external API, truncates a specified - ArcGIS Online credentials (username and password) - `.env` file for configuration (see below for details) -### Install dependencies +## Install Dependencies -You can install the required dependencies using `pip`: +To install the required dependencies, use the following command: ```bash pip install -r requirements.txt ``` +Alternatively, you can install the necessary packages individually: + +```bash +pip install requests +pip install python-dotenv +``` + ## Configuration -Before running the script, you'll need to configure some environment variables. Create a `.env` file with the following details: +Before running the script, you need to configure some environment variables. Create a `.env` file in the root of your project with the following details: ```env API_URL=your_api_url @@ -35,9 +42,10 @@ HOSTNAME=your_arcgis_host INSTANCE=your_arcgis_instance FS=your_feature_service LAYER=your_layer_id +LOG_LEVEL=your_log_level # e.g., DEBUG, INFO, WARNING, ERROR, CRITICAL ``` -### Variables +### Environment Variables: - **API_URL**: The URL of the API you are fetching data from. - **AGOL_USER**: Your ArcGIS Online username. @@ -46,6 +54,7 @@ LAYER=your_layer_id - **INSTANCE**: The instance name of your ArcGIS Online service. - **FS**: The name of the feature service you are working with. - **LAYER**: The ID or name of the layer to truncate and add features to. +- **LOG_LEVEL**: The desired logging level (e.g., `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`). ## Script Usage @@ -55,36 +64,34 @@ You can run the script with the following command: python 753DataSync.py --results_per_page ``` -### Arguments +### Arguments: -- **--results_per_page** (optional): The number of results to fetch per page (default: 100). +- `--results_per_page` (optional): The number of results to fetch per page (default: 100). ## Functionality -1. **Truncate Layer**: Before fetching and adding any new data, the script will call the `truncate` function to clear out any existing features from the specified layer. This ensures that the feature layer is empty and ready for the new data. +### 1. **Truncate Layer**: +Before fetching and adding any new data, the script will call the `truncate` function to clear out any existing features from the specified layer. This ensures that the feature layer is empty and ready for the new data. -2. **Fetch Data**: The script will then fetch data from the specified API in pages. Each page is fetched sequentially until all data is retrieved. +### 2. **Fetch Data**: +The script will then fetch data from the specified API in pages. Each page is fetched sequentially until all data is retrieved. -3. **Save Data**: Data from each page will be saved to an individual JSON file, with the filename including the page number and timestamp. The aggregated data (all pages combined) is saved to a separate file. +### 3. **Save Data**: +Data from each page will be saved to an individual JSON file, with the filename including the page number and timestamp. The aggregated data (all pages combined) is saved to a separate file. -4. **Add Features**: After all the data has been fetched and saved, the script will send the aggregated data as features to the specified ArcGIS feature layer. +### 4. **Add Features**: +After all the data has been fetched and saved, the script will send the aggregated data as features to the specified ArcGIS feature layer. -### Example Output +## Example Output - Individual page files are saved in the `data/` directory with filenames like `enforcement_page_1_results_100_2025-03-26_14-30-45.json`. - The aggregated file is saved as `aggregated_enforcement_results_2025-03-26_14-30-45.json`. - + Logs will also be generated in the `753DataSync.log` file and printed to the console. -## Error Handling - -- If an error occurs while fetching data, the script will log the error and stop execution. -- If the `truncate` or `add_features` operations fail, the script will log the error and stop execution. -- The script handles HTTP errors and network-related errors gracefully. - ## Example Output (Log) -``` +```text 2025-03-26 14:30:45 - INFO - Attempting to truncate layer on https://www.arcgis.com/... 2025-03-26 14:30:50 - INFO - Successfully truncated layer: https://www.arcgis.com/... 2025-03-26 14:30:51 - INFO - Making request to: https://api.example.com/1/100 @@ -94,6 +101,14 @@ Logs will also be generated in the `753DataSync.log` file and printed to the con 2025-03-26 14:31:00 - INFO - Features added successfully. ``` +## Error Handling + +The script handles errors gracefully, including: + +- If an error occurs while fetching data, the script will log the error and stop execution. +- If the `truncate` or `add_features` operations fail, the script will log the error and stop execution. +- The script handles HTTP errors and network-related errors gracefully, ensuring that any issues are logged with detailed information. + ## Troubleshooting - If the script stops unexpectedly, check the logs (`753DataSync.log`) for detailed error information. @@ -102,4 +117,4 @@ Logs will also be generated in the `753DataSync.log` file and printed to the con ## License -This project is licensed under the **GNU General Public License v3.0** or later - see the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the GNU General Public License v3.0 or later - see the [LICENSE](LICENSE) file for details. \ No newline at end of file -- 2.45.2 From 1bfa53223d4398bb91c01f250491837bfaaba31b Mon Sep 17 00:00:00 2001 From: Nick Heppler Date: Tue, 1 Apr 2025 19:48:25 -0400 Subject: [PATCH 4/5] Add logo file --- logo.png | Bin 0 -> 6867 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 logo.png diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..93a31f0150e061419bd2cc6995f540b3bc27a810 GIT binary patch literal 6867 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 N002ovPDHLkV1kk Date: Tue, 1 Apr 2025 19:48:44 -0400 Subject: [PATCH 5/5] Add release badge, logo to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 61ce81a..808fe8b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ +![753 Data Sync logo](https://git.nickhepler.cloud/nick/gotifyer/raw/branch/master/logo.png) # 753 Data Sync +![Gitea Release](https://img.shields.io/gitea/v/release/nick/753-Data-Sync?gitea_url=https%3A%2F%2Fgit.nickhepler.cloud) This script fetches enforcement data from an external API, truncates a specified feature layer in ArcGIS, and adds the fetched data as features to the layer. The script performs the following tasks: -- 2.45.2