diff --git a/.github/workflows/readme.yml b/.github/workflows/readme.yml new file mode 100644 index 00000000..199cb440 --- /dev/null +++ b/.github/workflows/readme.yml @@ -0,0 +1,56 @@ +name: Validate README Code Examples + +on: + pull_request: + branches: [ main ] + paths: + - 'pyTransition/examples/**' + - 'pyTransition/README.md' + - '.github/workflows/readme.yml' + - 'pyTransition/readme-examples-config.yaml' + - 'pyTransition/code-to-readme.py' + push: + branches: [ main ] + paths: + - 'pyTransition/examples/**' + - 'pyTransition/README.md' + - '.github/workflows/readme.yml' + - 'pyTransition/readme-examples-config.yaml' + - 'pyTransition/code-to-readme.py' + +jobs: + check-readme: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyyaml + + - name: Create temporary README copy + run: cp README.md README.md.orig + working-directory: ./pyTransition + + - name: Update README + run: python code-to-readme.py --readme README.md --config readme-examples-config.yaml + working-directory: ./pyTransition + + - name: Check if README was modified + working-directory: ./pyTransition + run: | + if ! diff -q README.md README.md.orig >/dev/null 2>&1; then + echo "README.md is out of sync with source code" + echo "diff:" + diff README.md README.md.orig || true + exit 1 + else + echo "README.md is up to date" + fi diff --git a/pyTransition/README.md b/pyTransition/README.md index db74fdeb..144c4239 100644 --- a/pyTransition/README.md +++ b/pyTransition/README.md @@ -179,6 +179,7 @@ This method allows users to send accessibility map parameters to the Transition ## Example Users can fetch the nodes which are currently loaded in the Transition application using pyTransition as follows : + ```python from pyTransition.transition import Transition @@ -193,30 +194,36 @@ def get_transition_nodes(): # Process nodes however you want. Here, we are just printing the result print(nodes) ``` + + Fetching the paths can be done in the same way, by replacing *get_nodes()* with *get_paths()*. Alternatively, if the user already knows their Transition API authentication token, they can create the instance with it directly, as follows : + ```python from pyTransition.transition import Transition -def get_transition_nodes(): +# Get a token for later testing +transition_instance_token = Transition("http://localhost:8080", username, password) +token = transition_instance_token.token + +# Alternative version with token +def get_transition_nodes_with_token(): # Create Transition instance from authentication token # The login information can be saved in a file to not have them displayed in the code transition_instance = Transition("http://localhost:8080", None, None, token) # Call the API - nodes = Transition.get_nodes() + nodes = transition_instance.get_nodes() # Process nodes however you want. Here, we are just printing the result print(nodes) ``` + Another example using pyTransition to get a new accessibility map : + ```python -from pyTransition.transition import Transition -from datetime import time -import json - def get_transition_acessibility_map(): # Create Transition instance from connection credentials transition_instance = Transition("http://localhost:8080", username, password) @@ -225,11 +232,11 @@ def get_transition_acessibility_map(): scenarios = transition_instance.get_scenarios() # Get the ID of the scenario we want to use. Here, we use the first one - scenario_id = scenarios['collection'][0]['id'] + scenario_id = scenarios[0]['id'] # Call the API accessibility_map_data = transition_instance.request_accessibility_map( - coordinates=[45.5383, -73.4727], + coordinates=[-73.4727, 45.5383], departure_or_arrival_choice="Departure", departure_or_arrival_time=time(8,0), # Create a new time object representing 8:00 n_polygons=3, @@ -248,15 +255,12 @@ def get_transition_acessibility_map(): # Process the map however you want. Here, we are saving it to a json file with open("accessibility.json", 'w') as f: f.write(json.dumps(accessibility_map_data)) - ``` + Another example using pyTransition to get a new routes : + ```python -from pyTransition.transition import Transition -from datetime import time -import json - def get_transition_routes(): # Create Transition instance from connection credentials # The login information can be saved in a file to not have them displayed in the code @@ -268,44 +272,42 @@ def get_transition_routes(): routing_modes = transition_instance.get_routing_modes() # Get the ID of the scenario we want to use. Here, we use the first one - scenario_id = scenarios['collection'][0]['id'] - # Get the modes you want to use. Here, we are usisng the first two ones + scenario_id = scenarios[0]['id'] + # Get the modes you want to use. Here, we are using the first two ones # You can print the modes to see which are available modes = routing_modes[:2] - # Call the API routing_data = transition_instance.request_routing_result( modes=modes, origin=[-73.4727, 45.5383], destination=[-73.4499, 45.5176], - scenario_id=scenarioId, + scenario_id=scenario_id, departure_or_arrival_choice=departureOrArrivalChoice, departure_or_arrival_time=departureOrArrivalTime, max_travel_time_minutes=maxParcoursTime, min_waiting_time_minutes=minWaitTime, max_transfer_time_minutes=maxTransferWaitTime, max_access_time_minutes=maxAccessTimeOrigDest, - max_first_waiting_time_minutes=maxWaitTimeFisrstStopChoice, + max_first_waiting_time_minutes=maxWaitTimeFirstStopChoice, with_geojson=True, with_alternatives=True ) # Process the data however you want. - # For example, we can get the geojson paths of each transit mode in a loop - for key, value in routing_data.items(): + # For each alternative, get the geojson associated + for key, value in routing_data['result'].items(): # Get the number of alternative paths for the current mode geojsonPaths = value["pathsGeojson"] mode = key # For each alternative, get the geojson associated - for i in range(len(geojsonPath)): - geojson_data = geojsonPath[i] + for geojson_data in geojsonPaths: + # Process however you want. Here we are just printing it. print(geojson_data) # We can also save it to a json file with open("routing.json", 'w') as f: f.write(json.dumps(routing_data)) - - ``` + diff --git a/pyTransition/code-to-readme.py b/pyTransition/code-to-readme.py new file mode 100644 index 00000000..48aa7bc7 --- /dev/null +++ b/pyTransition/code-to-readme.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 + +# MIT License + +# Copyright (c) 2024 Polytechnique Montréal + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" + +Code to README Integration Tool +============================== +(This was done with claude.ai) + + +This script helps maintain code examples in README files by automatically extracting +and inserting code snippets from source files. It uses markers in both the README +and source files to determine where code should be inserted. + +Usage +----- +```bash +python code_to_readme.py --readme README.md --config examples.yaml +``` + +Configuration File (examples.yaml) +-------------------------------- +The configuration file should be in YAML format and specify which code blocks +should be inserted where. Example structure: + +```yaml +examples: + - block_id: basic_example # Unique identifier for this code block + path: src/example.py # Path to the source file + markers: # Optional: if not provided, includes entire file + - start: "# START basic" # Start marker in source file + end: "# END basic" # End marker in source file + include_markers: false # Whether to include markers in output + language: python # Optional: override language detection + + - block_id: full_file # Example without markers + path: src/util.py # Will include the entire file +``` + +README Format +------------ +In your README.md, place markers where you want code to be inserted: + +```markdown + +```python +# Code will be automatically inserted here +``` + +``` + +Source File Format +----------------- +In your source files, mark the code sections you want to extract (if using markers): + +```python +# Other code... + +# START basic +def example(): + print("This will be extracted") +# END basic + +# More code... +``` + +Features +-------- +- Extract whole files or specific marked sections +- Optional inclusion of marker comments +- Language auto-detection based on file extension +- Multiple code blocks per file +- Preserve README formatting outside of marked sections +""" + +import re +from pathlib import Path +import argparse +import yaml +from dataclasses import dataclass +from typing import List, Dict, Optional, Tuple + +@dataclass +class MarkerPair: + start: str + end: str + include_markers: bool = False + +@dataclass +class CodeExample: + path: str + block_id: str + markers: Optional[List[MarkerPair]] = None + language: Optional[str] = None + +class CodeToReadmeIntegrator: + def __init__(self, readme_path: str): + self.readme_path = Path(readme_path) + + @staticmethod + def load_config(config_path: str) -> Dict[str, CodeExample]: + """Load example files configuration from YAML file.""" + with open(config_path, 'r') as f: + config = yaml.safe_load(f) + + examples = {} + for item in config['examples']: + # Convert markers configuration to MarkerPair objects + markers = None + if 'markers' in item: + markers = [ + MarkerPair( + start=m['start'], + end=m['end'], + include_markers=m.get('include_markers', False) + ) for m in item['markers'] + ] + + examples[item['block_id']] = CodeExample( + path=item['path'], + block_id=item['block_id'], + markers=markers, + language=item.get('language') + ) + return examples + + def detect_language(self, file_path: str, override: Optional[str] = None) -> str: + """Detect language based on file extension or override.""" + if override: + return override + + extension_map = { + '.py': 'python', + '.js': 'javascript', + '.ts': 'typescript', + '.sh': 'bash', + '.java': 'java', + '.rb': 'ruby', + '.go': 'go', + '.rs': 'rust', + '.cpp': 'cpp', + '.c': 'c', + } + return extension_map.get(Path(file_path).suffix, 'text') + + def extract_code(self, content: str, markers: Optional[List[MarkerPair]]) -> str: + """Extract code between markers or return full content.""" + if not markers: + return content.strip() + + extracted = [] + lines = content.split('\n') + capturing = False + current_snippet = [] + current_marker = None + + for line in lines: + # Check for start markers + for marker in markers: + if marker.start in line: + capturing = True + current_marker = marker + if marker.include_markers: + current_snippet.append(line) + break + elif marker.end in line: + if marker == current_marker: + if marker.include_markers: + current_snippet.append(line) + capturing = False + current_marker = None + if current_snippet: + extracted.append('\n'.join(current_snippet)) + current_snippet = [] + break + else: # no marker found in this line + if capturing: + current_snippet.append(line) + + return '\n\n'.join(extracted).strip() + + def update_readme(self, examples: Dict[str, CodeExample]) -> None: + """Update README file with code examples.""" + with open(self.readme_path, 'r') as f: + content = f.read() + + for block_id, example in examples.items(): + # Pattern to match content between markers including the markers + pattern = f"().*()" + + try: + # Read the source file + with open(example.path, 'r') as f: + code = f.read() + + # Extract relevant code if markers are specified + code = self.extract_code(code, example.markers) + + # Detect language + language = self.detect_language(example.path, example.language) + + # Create the replacement block + replacement = f""" +```{language} +{code} +``` +""" + + # Replace in README + content = re.sub(pattern, replacement, content, flags=re.DOTALL) + + except FileNotFoundError: + print(f"Warning: Source file not found: {example.path}") + except Exception as e: + print(f"Error processing block {block_id}: {str(e)}") + + # Write updated content + with open(self.readme_path, 'w') as f: + f.write(content) + +def main(): + parser = argparse.ArgumentParser(description='Integrate code files into README') + parser.add_argument('--readme', required=True, help='Path to README.md') + parser.add_argument('--config', required=True, help='Path to examples configuration YAML') + + args = parser.parse_args() + + integrator = CodeToReadmeIntegrator(args.readme) + examples = integrator.load_config(args.config) + integrator.update_readme(examples) + +if __name__ == "__main__": + main() diff --git a/pyTransition/examples/README.md b/pyTransition/examples/README.md new file mode 100644 index 00000000..a57d0056 --- /dev/null +++ b/pyTransition/examples/README.md @@ -0,0 +1,9 @@ +# Running the examples + +To run the examples, first edit the test_credentials.py file with the right +username and password for your setup. + +Then you ran run them while setting the PYTHONPATH: +``` +PYTHONPATH=.. python3 routing.py +``` \ No newline at end of file diff --git a/pyTransition/examples/accessibility_map.py b/pyTransition/examples/accessibility_map.py new file mode 100644 index 00000000..ee1ad075 --- /dev/null +++ b/pyTransition/examples/accessibility_map.py @@ -0,0 +1,39 @@ +from test_credentials import username, password +from pyTransition.transition import Transition +from datetime import time +import json + +def get_transition_acessibility_map(): + # Create Transition instance from connection credentials + transition_instance = Transition("http://localhost:8080", username, password) + + # Get the scenarios. A scenario is needed to request an accessibility map + scenarios = transition_instance.get_scenarios() + + # Get the ID of the scenario we want to use. Here, we use the first one + scenario_id = scenarios[0]['id'] + + # Call the API + accessibility_map_data = transition_instance.request_accessibility_map( + coordinates=[-73.4727, 45.5383], + departure_or_arrival_choice="Departure", + departure_or_arrival_time=time(8,0), # Create a new time object representing 8:00 + n_polygons=3, + delta_minutes=15, + delta_interval_minutes=5, + scenario_id=scenario_id, + max_total_travel_time_minutes=30, + min_waiting_time_minutes=3, + max_access_egress_travel_time_minutes=15, + max_transfer_travel_time_minutes=10, + max_first_waiting_time_minutes=0, + walking_speed_kmh=5, + with_geojson=True, + ) + + # Process the map however you want. Here, we are saving it to a json file + with open("accessibility.json", 'w') as f: + f.write(json.dumps(accessibility_map_data)) + +if __name__ == "__main__": + get_transition_acessibility_map() diff --git a/pyTransition/examples/basic_usage.py b/pyTransition/examples/basic_usage.py new file mode 100644 index 00000000..8a0dae1e --- /dev/null +++ b/pyTransition/examples/basic_usage.py @@ -0,0 +1,17 @@ +from test_credentials import username, password + +from pyTransition.transition import Transition + +def get_transition_nodes(): + # Create Transition instance from connection credentials + # The login information can be saved in a file to not have them displayed in the code + transition_instance = Transition("http://localhost:8080", username, password) + + # Call the API + nodes = transition_instance.get_nodes() + + # Process nodes however you want. Here, we are just printing the result + print(nodes) + +if __name__ == "__main__": + get_transition_nodes() diff --git a/pyTransition/examples/basic_usage_token.py b/pyTransition/examples/basic_usage_token.py new file mode 100644 index 00000000..04c895b9 --- /dev/null +++ b/pyTransition/examples/basic_usage_token.py @@ -0,0 +1,22 @@ +from test_credentials import username, password + +from pyTransition.transition import Transition + +# Get a token for later testing +transition_instance_token = Transition("http://localhost:8080", username, password) +token = transition_instance_token.token + +# Alternative version with token +def get_transition_nodes_with_token(): + # Create Transition instance from authentication token + # The login information can be saved in a file to not have them displayed in the code + transition_instance = Transition("http://localhost:8080", None, None, token) + + # Call the API + nodes = transition_instance.get_nodes() + + # Process nodes however you want. Here, we are just printing the result + print(nodes) + +if __name__ == "__main__": + get_transition_nodes_with_token() diff --git a/pyTransition/examples/routing.py b/pyTransition/examples/routing.py new file mode 100644 index 00000000..35f8844b --- /dev/null +++ b/pyTransition/examples/routing.py @@ -0,0 +1,64 @@ +from test_credentials import username, password +from pyTransition.transition import Transition +from datetime import time +import json + +departureOrArrivalChoice = "Departure" +departureOrArrivalTime = time(8,0) +maxParcoursTime = 60 +minWaitTime = 15 +maxTransferWaitTime = 15 +maxAccessTimeOrigDest = 10 +maxWaitTimeFirstStopChoice = 5 + +def get_transition_routes(): + # Create Transition instance from connection credentials + # The login information can be saved in a file to not have them displayed in the code + transition_instance = Transition("http://localhost:8080", username, password) + + # Get the scenarios and routing modes. A scenario and at least one routing mode + # are needed to request an new route + scenarios = transition_instance.get_scenarios() + routing_modes = transition_instance.get_routing_modes() + + # Get the ID of the scenario we want to use. Here, we use the first one + scenario_id = scenarios[0]['id'] + # Get the modes you want to use. Here, we are using the first two ones + # You can print the modes to see which are available + modes = routing_modes[:2] + + # Call the API + routing_data = transition_instance.request_routing_result( + modes=modes, + origin=[-73.4727, 45.5383], + destination=[-73.4499, 45.5176], + scenario_id=scenario_id, + departure_or_arrival_choice=departureOrArrivalChoice, + departure_or_arrival_time=departureOrArrivalTime, + max_travel_time_minutes=maxParcoursTime, + min_waiting_time_minutes=minWaitTime, + max_transfer_time_minutes=maxTransferWaitTime, + max_access_time_minutes=maxAccessTimeOrigDest, + max_first_waiting_time_minutes=maxWaitTimeFirstStopChoice, + with_geojson=True, + with_alternatives=True + ) + + # Process the data however you want. + # For each alternative, get the geojson associated + for key, value in routing_data['result'].items(): + # Get the number of alternative paths for the current mode + geojsonPaths = value["pathsGeojson"] + mode = key + # For each alternative, get the geojson associated + for geojson_data in geojsonPaths: + + # Process however you want. Here we are just printing it. + print(geojson_data) + + # We can also save it to a json file + with open("routing.json", 'w') as f: + f.write(json.dumps(routing_data)) + +if __name__ == "__main__": + get_transition_routes() diff --git a/pyTransition/examples/test_credentials.py b/pyTransition/examples/test_credentials.py new file mode 100644 index 00000000..2cc2b858 --- /dev/null +++ b/pyTransition/examples/test_credentials.py @@ -0,0 +1,2 @@ +username = "test" +password = "test" diff --git a/pyTransition/readme-examples-config.yaml b/pyTransition/readme-examples-config.yaml new file mode 100644 index 00000000..91bbf060 --- /dev/null +++ b/pyTransition/readme-examples-config.yaml @@ -0,0 +1,28 @@ +examples: + - block_id: basic_usage + path: examples/basic_usage.py + markers: + - start: "from pyTransition.transition import Transition" + end: " print(nodes)" + include_markers: true + + - block_id: basic_usage_token + path: examples/basic_usage_token.py + markers: + - start: "from pyTransition.transition import Transition" + end: " print(nodes)" + include_markers: true + + - block_id: accessibility_map + path: examples/accessibility_map.py + markers: + - start: "def get_transition_acessibility_map():" + end: " f.write(json.dumps(accessibility_map_data))" + include_markers: true + + - block_id: routing + path: examples/routing.py + markers: + - start: "def get_transition_routes():" + end: " f.write(json.dumps(routing_data))" + include_markers: true