Skip to content

Commit 1268c19

Browse files
committed
Updates
1 parent 796bb73 commit 1268c19

File tree

2 files changed

+365
-0
lines changed

2 files changed

+365
-0
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
---
2+
layout: post
3+
title: Dynamic Module Loading in Python
4+
date: 2024-10-24 19:01
5+
categories: [Guides, Software]
6+
tags: [guide, development]
7+
---
8+
9+
In the Modular Biped Project I wanted to achieve a **cleaner, scalable**, and **modular design** where new modules can be added or removed without needing to modify `main.py`. To achieve this I wanted to refactor the initiation logic into **module-specific files**. This approach allows each module to handle its own initialization independently, making `main.py` generic and less dependent on changes as new modules are introduced.
10+
11+
# Dynamically Loading Python Modules: A Flexible Architecture for Configurable Module Loading
12+
13+
In modern applications, flexibility and scalability are key to adapting to new requirements and functionalities. One effective way to manage growing complexity is to design an architecture that dynamically loads modules based on configuration files—enabling the system to remain adaptable without altering core logic. In this article, we will discuss how to build such a system in Python, allowing modules to be dynamically loaded at runtime, based on YAML configuration files. We'll also explore how to extend the system by writing new modules that fit seamlessly into this architecture.
14+
15+
## The Problem: Static Module Loading
16+
17+
In many Python applications, modules are statically imported and initialized. This approach works fine for small applications, but as the number of modules grows, managing them manually in the main script can become a nightmare. Every time a module is added, removed, or modified, you need to update the `main.py` file or other related scripts.
18+
19+
Consider the following scenario:
20+
- You have a robotics system that uses multiple modules like actuators, sensors, and camera systems.
21+
- The system is configurable using YAML files, with some modules needing multiple instances (e.g., multiple servo motors).
22+
- You want the system to automatically load and initialize modules based on whether they are enabled in the configuration files, without having to modify the core application.
23+
24+
## Enter: Dynamic Module Loading
25+
26+
In our approach, we will design an architecture that dynamically loads Python modules based on the contents of YAML configuration files. Each module will be able to accept configuration parameters passed dynamically as `**kwargs`, allowing flexibility in how modules are instantiated and configured.
27+
28+
## The Architecture: Dynamic Module Loader
29+
30+
Our architecture revolves around three key components:
31+
1. **YAML Configuration Files**: Define which modules are enabled and their configuration details.
32+
2. **ModuleLoader Class**: Handles the discovery and dynamic loading of modules based on the YAML configuration.
33+
3. **Module Initialization with `**kwargs`**: Ensures that configuration options are passed dynamically to module constructors.
34+
35+
### Folder Structure
36+
37+
```
38+
my_project/
39+
40+
├── config/
41+
│ ├── servos.yml # Configuration for servo modules
42+
│ └── buzzer.yml # Configuration for buzzer module
43+
44+
├── modules/
45+
│ ├── actuators/
46+
│ │ └── servo.py # Implementation of the Servo class
47+
│ │
48+
│ ├── sensor/
49+
│ │ └── motion_sensor.py # Implementation of the MotionSensor class
50+
│ │
51+
│ ├── output/
52+
│ │ ├── buzzer.py # Implementation of the Buzzer class
53+
│ │ └── speaker.py # Implementation of the Speaker class (if needed)
54+
│ │
55+
│ └── ... # Other module directories as needed
56+
57+
├── main.py # Entry point for the application
58+
├── module_loader.py # Contains the ModuleLoader class
59+
└── requirements.txt # List of dependencies for the project
60+
```
61+
62+
### 1. YAML Configuration Files
63+
64+
Each module has a corresponding YAML configuration file that defines whether the module is enabled and what parameters it should use. For example, a `servos.yml` file might configure multiple servo motor instances:
65+
66+
```yaml
67+
servos:
68+
enabled: true
69+
path: "modules/actuators/servo"
70+
instances:
71+
- name: "leg_l_hip"
72+
id: 0
73+
pin: 9
74+
range: [0, 180]
75+
start_pos: 40
76+
- name: "leg_l_knee"
77+
id: 1
78+
pin: 10
79+
range: [0, 180]
80+
start_pos: 10
81+
```
82+
83+
In this example, the `servos` module is enabled, and two instances (`leg_l_hip` and `leg_l_knee`) are defined with their respective configurations. The `path` points to where the module is located within the project folder.
84+
85+
### 2. ModuleLoader Class
86+
87+
The `ModuleLoader` class is responsible for reading these YAML files, dynamically loading the modules, and creating instances based on the configuration provided.
88+
89+
Here’s an implementation of `ModuleLoader`:
90+
91+
```python
92+
import os
93+
import yaml
94+
import importlib.util
95+
96+
class ModuleLoader:
97+
def __init__(self, config_folder='config'):
98+
self.config_folder = config_folder
99+
self.modules = self.load_yaml_files()
100+
101+
def load_yaml_files(self):
102+
"""Load and parse YAML files from the config folder."""
103+
config_files = [os.path.join(self.config_folder, f) for f in os.listdir(self.config_folder) if f.endswith('.yml')]
104+
loaded_modules = []
105+
for file_path in config_files:
106+
with open(file_path, 'r') as stream:
107+
try:
108+
config = yaml.safe_load(stream)
109+
for module_name, module_config in config.items():
110+
if module_config.get('enabled', False):
111+
loaded_modules.append(module_config)
112+
except yaml.YAMLError as e:
113+
print(f"Error loading {file_path}: {e}")
114+
return loaded_modules
115+
116+
def load_modules(self):
117+
"""Dynamically load and instantiate the modules based on the config."""
118+
instances = {} # Use a dictionary to store instances for easy access
119+
for module in self.modules:
120+
module_path = module['path'] # e.g., "modules/actuators/servo"
121+
module_name = module_path.split('/')[-1] # e.g., "servo"
122+
instances_config = module.get('instances', [module.get('config')]) # Get all instances or config
123+
124+
# Dynamically load the module
125+
spec = importlib.util.spec_from_file_location(module_name, f"{module_path}/{module_name}.py")
126+
mod = importlib.util.module_from_spec(spec)
127+
spec.loader.exec_module(mod)
128+
129+
# Create instances of the module
130+
for instance_config in instances_config:
131+
# Pass the instance config to the module's __init__ method as **kwargs
132+
instance_name = instance_config.get('name') # Use the instance name as the key
133+
instance = getattr(mod, module_name.capitalize())(**instance_config)
134+
135+
# Store the instance in the dictionary
136+
instances[instance_name] = instance
137+
138+
return instances # Return the dictionary of instances
139+
140+
```
141+
142+
#### How It Works:
143+
1. **Loading YAML Files**: The `ModuleLoader` reads all YAML files from the `config` folder and loads only the modules that are marked as `enabled`.
144+
2. **Dynamic Module Loading**: Using Python’s `importlib`, the `ModuleLoader` dynamically loads the Python files based on the module path in the YAML.
145+
3. **Module Initialization**: The `ModuleLoader` passes the configuration for each instance as `**kwargs` to the module’s constructor.
146+
147+
### 3. Module Initialization with `**kwargs`
148+
149+
For each module, the constructor (`__init__` method) should be designed to accept `**kwargs`, which allows it to handle configurations flexibly. Let’s look at an example module for controlling servo motors.
150+
151+
```python
152+
class Servo:
153+
def __init__(self, **kwargs):
154+
self.pin = kwargs.get('pin') # Required, no default
155+
self.name = kwargs.get('name') # Required, no default
156+
self.range = kwargs.get('range', [0, 180]) # Optional with default value
157+
self.id = kwargs.get('id', 0) # Optional with default value
158+
self.start = kwargs.get('start_pos', 50) # Optional with default value
159+
print(f"Initializing Servo {self.name} on pin {self.pin} with range {self.range} and start {self.start}")
160+
```
161+
162+
Here, the `**kwargs` argument makes the `Servo` class highly flexible, allowing it to accept any configuration that is passed from the YAML file. Using `kwargs.get()`, we retrieve specific configuration parameters and assign default values when necessary.
163+
164+
### 4. Simplified `main.py`
165+
166+
Here, the `main.py` script is simplified to focus on the dynamic loading and initialization of modules, without needing to hard-code specific modules or configurations. Any changes to modules or configurations are now handled entirely via the YAML files.
167+
168+
If you wanted direct access to the instance created by the model loader, you can do that too! In the `main.py` file, when the `ModuleLoader` loads the modules, we have them ready for use:
169+
170+
```python
171+
from module_loader import ModuleLoader
172+
173+
def main():
174+
# Load all enabled modules
175+
module_loader = ModuleLoader(config_folder='config')
176+
module_instances = module_loader.load_modules()
177+
178+
# Access instances by name
179+
my_module = module_instances.get("my_module")
180+
if my_module:
181+
my_module.start() # Assuming there's a start method
182+
183+
if __name__ == '__main__':
184+
main()
185+
186+
```
187+
188+
Interacting with your module via pubsub is also possible, and avoids adding business logic to the main.py file.
189+
190+
```python
191+
from pubsub import pub
192+
pub.sendMessage('mytopic', data='somedata') # Publish to a topic
193+
pub.subscribe(self.handler_method, 'anothertopic') # subscribe to another topic
194+
```
195+
196+
## Writing a New Module for This Architecture
197+
198+
To add a new module to this architecture, follow these simple steps:
199+
200+
### 1. Create a Python Module
201+
202+
Create a new Python class for your module. Ensure that the `__init__` method accepts `**kwargs` for dynamic configuration.
203+
204+
#### Example: `buzzer.py`
205+
206+
```python
207+
class Buzzer:
208+
def __init__(self, **kwargs):
209+
self.pin = kwargs.get('pin') # Required, no default
210+
print(f"Initializing Buzzer on pin {self.pin}")
211+
212+
def buzz(self):
213+
print(f"Buzzer on pin {self.pin} is buzzing!")
214+
```
215+
216+
### 2. Define a YAML Configuration
217+
218+
Create a corresponding YAML configuration file in the `config` folder that specifies the module's configuration. For example, the `buzzer.yml` file might look like this:
219+
220+
```yaml
221+
buzzer:
222+
enabled: true
223+
path: "modules/output/buzzer"
224+
config:
225+
pin: 26
226+
```
227+
228+
### 3. Load the Module Dynamically
229+
230+
With the `ModuleLoader` system in place, the module will be automatically loaded when the application runs, provided that it is enabled in its YAML configuration. No changes are needed in `main.py` or elsewhere in the core logic.
231+
232+
## Conclusion
233+
234+
By introducing dynamic module loading based on YAML configuration files, we’ve built an architecture that is flexible, scalable, and easy to extend. This approach allows developers to add new modules, enable or disable them, and configure multiple instances of the same module—all without modifying the core application code.
235+
236+
### Key Benefits:
237+
- **Scalability**: Easily add or remove modules without changing the core logic.
238+
- **Flexibility**: Modules can accept any configuration parameters dynamically using `**kwargs`.
239+
- **Maintainability**: Centralized management of configuration via YAML files.
240+
241+
Now, when your system grows or needs new modules, you can simply drop in the new module and corresponding YAML file, making your Python application truly dynamic and adaptable.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
---
2+
layout: post
3+
title: Using SSH without passwords
4+
date: 2024-10-23 19:24
5+
categories: [Guides, Software]
6+
tags: [guide, development]
7+
---
8+
9+
I love being able to remote into my Raspberry Pi, but sometimes entering the username / password combo can be time consuming, especially after multiple restarts. Let's take a look at a way to store your credentials so that the connection can be made easily.
10+
11+
## Setting Up SSH Passwordless Login: A Step-by-Step Guide for Makers
12+
13+
Secure Shell (SSH) is a powerful tool that allows users to perform a wide range of tasks remotely, from accessing servers to automating backups and synchronizing files. One of the most efficient and secure ways to manage these tasks is by setting up SSH passwordless login, which uses a pair of cryptographic keys to authenticate users without needing to enter a password each time.
14+
15+
In this guide, we’ll walk you through the process of setting up SSH passwordless login on a Linux server. By the end, you’ll be able to connect securely without repeatedly typing your password, while also maintaining security by using a public and private key pair.
16+
17+
### Why Use SSH Passwordless Login?
18+
19+
SSH passwordless login improves security and convenience, but it can come with challenges:
20+
- **Rotating credentials**: When team members come and go, new credentials (either passwords or keys) must be created, and the old ones removed.
21+
- **Auditing access**: While SSH is secure, wrapping communication in an SSH tunnel can make tracking and managing access more difficult.
22+
- **Credential rotation**: Keeping keys updated can be a time-consuming and overlooked task.
23+
24+
Luckily, automating these processes can make managing SSH keys much easier, as we'll cover later.
25+
26+
### Setting Up SSH Passwordless Login
27+
28+
Let’s get started on configuring SSH passwordless login. The following steps apply to most modern Linux distributions and include specific instructions for macOS and Windows clients as well.
29+
30+
#### Step 1: Generate a Key Pair
31+
32+
To establish SSH passwordless login, you first need to generate a public and private key pair on your client machine.
33+
34+
In a terminal (Linux/macOS) or Command Prompt (Windows), enter:
35+
36+
```bash
37+
ssh-keygen -t rsa
38+
```
39+
40+
This command tells the system to generate an RSA key. While RSA is the most common, you can also choose from other algorithms like DSA, ECDSA, or ED25519 depending on your setup.
41+
42+
You’ll be prompted to specify a file name for the keys:
43+
44+
```bash
45+
Enter file in which to save the key (C:\Users\your_username\.ssh\id_rsa):
46+
```
47+
48+
It’s fine to press `Enter` here and use the default path. If this is your first key or primary key, it’s recommended to stick with the default `id_rsa` naming convention.
49+
50+
Next, you’ll be asked to create a passphrase. This step is essential to securing your private key—while optional, it’s highly recommended to add a passphrase to protect the key if it’s ever compromised.
51+
52+
```bash
53+
Enter passphrase (empty for no passphrase):
54+
```
55+
56+
Choose a secure passphrase, press `Enter`, and confirm it when prompted. You’ll now have two files:
57+
- `id_rsa`: Your private key.
58+
- `id_rsa.pub`: Your public key.
59+
60+
#### Step 2: Set Up the SSH Directory on the Server
61+
62+
Now, it’s time to set up the server to accept your public key. First, log into your server using SSH with your existing username and password. Once connected, check if the `.ssh` directory exists:
63+
64+
```bash
65+
ls .ssh
66+
```
67+
68+
If the directory doesn’t exist, create it:
69+
70+
```bash
71+
mkdir -p .ssh
72+
```
73+
74+
The `-p` option ensures that the directory is created along with its parent directories if they don’t already exist.
75+
76+
#### Step 3: Upload the Public Key to the Server
77+
78+
Next, you’ll upload your public key to the server.
79+
80+
You’ll need to manually upload the key using the `scp` command. If your server doesn’t already have an `authorized_keys` file, use this command:
81+
82+
```bash
83+
scp .ssh/id_rsa.pub [email protected]:~/.ssh/authorized_keys
84+
```
85+
86+
If there is an existing `authorized_keys` file, append your key to avoid removing existing access:
87+
88+
1. Copy the key to the server:
89+
```bash
90+
scp .ssh/id_rsa.pub [email protected]:~/.ssh
91+
```
92+
93+
2. Log into the server and append the key:
94+
```bash
95+
cat .ssh/id_rsa.pub >> .ssh/authorized_keys
96+
```
97+
98+
3. Clean up the server:
99+
```bash
100+
rm .ssh/id_rsa.pub
101+
```
102+
103+
#### Step 4: Adjust Permissions and Test the Connection
104+
105+
Once the key is uploaded, ensure the correct file permissions are set. On the server, enter the following commands:
106+
107+
```bash
108+
chmod 700 ~/.ssh
109+
chmod 600 ~/.ssh/authorized_keys
110+
```
111+
112+
These commands ensure that only the owner can read or write to the `.ssh` directory and the `authorized_keys` file.
113+
114+
Now, disconnect from the server and try reconnecting:
115+
116+
```bash
117+
118+
```
119+
120+
### Wrapping Up
121+
122+
With SSH passwordless login configured, you’ve now streamlined your workflow for accessing servers securely and conveniently. Remember to regularly rotate your keys, back them up, and always use a passphrase to keep your private key secure.
123+
124+
For even more advanced setups, you can integrate SSH with automated tools and agents that further simplify access management and security auditing, taking your system to the next level. Happy making!

0 commit comments

Comments
 (0)