# python  for devops

Python is a high-level, versatile, and interpreted programming language known for its simplicity and readability. It was created by Guido van Rossum and first released in 1991. Python has gained popularity for a wide range of applications, from web development and data analysis to artificial intelligence and scientific computing.

Key characteristics and features of Python include:

**High-Level Language**: Python is a high-level language, which means it abstracts low-level details, making it more user-friendly and less concerned with hardware-specific operations.

**Interpreted Language**: Python code is executed by an interpreter, allowing for easy code testing and prototyping without the need for compilation.

**Cross-Platform**: Python is available on various platforms (Windows, macOS, Linux), making it highly portable.

**Rich Standard Library**: Python includes a comprehensive standard library with modules for a wide range of tasks, reducing the need to write custom code for common operations.

**Versatility**: Python can be used for a wide variety of applications, including web development (Django, Flask), data analysis (Pandas, NumPy), machine learning (TensorFlow, PyTorch), scientific computing (SciPy), automation, and more.

**Object-Oriented**: Python supports object-oriented programming, allowing developers to define classes and create objects.

Python's ease of use, wide range of applications, and strong community support have made it a popular choice for beginners and experienced programmers alike. Its popularity continues to grow, and it is widely used in fields such as web development, data science, artificial intelligence, and more.

**Compiler:**

1. **Role**: A compiler is a program that translates the entire source code of a high-level programming language into machine code or an intermediate code all at once before execution.
    

**Interpreter:**

1. **Role**: An interpreter is a program that reads and executes the source code line by line or statement by statement without creating an intermediate executable file.
    

I can teach you about Python data types. Python is a dynamically typed language, which means that the data type of a variable is determined at runtime. Here are some common data types in Python:

1. **Integers (int)**: Integers are whole numbers, like -3, -2, -1, 0, 1, 2, 3, etc. You can define them using the `int` keyword.
    
2. **Floating-Point Numbers (float)**: Floating-point numbers are numbers with decimal points, like 3.14, 2.71828, etc. You can define them using the `float` keyword.
    
3. **Strings (str)**: Strings are sequences of characters, like "Hello, World!" or 'Python'. You can define them using single or double quotes.
    
4. **Booleans (bool)**: Booleans represent either True or False. These are often used in conditional statements and logical operations. You can define them using the `True` or `False` keywords.
    
5. **Lists**: Lists are ordered collections of values. They can hold elements of different data types and are defined using square brackets, e.g., `[1, 2, 'three']`.
    
6. **Tuples**: Tuples are similar to lists but are immutable, meaning their elements cannot be changed once defined. They are defined using parentheses, e.g., `(1, 2, 'three')`.
    
7. **Dictionaries**: Dictionaries are key-value pairs, where each key is associated with a value. They are defined using curly braces, e.g., `{'name': 'John', 'age': 30}`.
    
8. **Sets**: Sets are unordered collections of unique elements. They are defined using curly braces or the `set()` constructor, e.g., `{1, 2, 3}`.
    
9. **None**: `None` is a special type representing the absence of a value or a null value.
    

You can use the `type()` function to determine the data type of a variable. For example:

```python
x = 5
print(type(x))  # This will output <class 'int'>
```

These are the fundamental data types in Python. You can also create your own custom data types using classes, which are a more advanced topic.

Certainly! Here are some examples of Python data types:

1. **Integers (int)**:
    

```python
x = 5
print(type(x))  # <class 'int'>
```

1. **Floating-Point Numbers (float)**:
    

```python
y = 3.14
print(type(y))  # <class 'float'>
```

1. **Strings (str)**:
    

```python
message = "Hello, World!"
print(type(message))  # <class 'str'>
```

1. **Booleans (bool)**:
    

```python
is_python_fun = True
print(type(is_python_fun))  # <class 'bool'>
```

1. **Lists**:
    

```python
fruits = ['apple', 'banana', 'cherry']
print(type(fruits))  # <class 'list'>
```

1. **Tuples**:
    

```python
coordinates = (3, 4)
print(type(coordinates))  # <class 'tuple'>
```

1. **Dictionaries**:
    

```python
person = {'name': 'John', 'age': 30}
print(type(person))  # <class 'dict'>
```

1. **Sets**:
    

```python
unique_numbers = {1, 2, 3, 4}
print(type(unique_numbers))  # <class 'set'>
```

1. **None**:
    

```python
empty_value = None
print(type(empty_value))  # <class 'NoneType'>
```

These examples demonstrate how to create and identify various data types in Python. You can perform operations and manipulate data using these data types in your Python programs.

**Mutable Data Types:**

**Lists (list)**:

* Mutable: Lists are ordered collections that can be modified by adding, removing, or changing elements.
    
* Use Case: Lists are commonly used for dynamic collections of items that can change in size.
    

Lists in Python are versatile and come with several built-in methods for various operations. Here are some common list methods with examples:

1. **append()**: Adds an element to the end of the list.
    

```python
fruits = ['apple', 'banana']
fruits.append('cherry')
# fruits is now ['apple', 'banana', 'cherry']
```

1. **extend()**: Extends a list by appending elements from another iterable.
    

```python
fruits = ['apple', 'banana']
fruits.extend(['cherry', 'date'])
# fruits is now ['apple', 'banana', 'cherry', 'date']
```

1. **insert()**: Inserts an element at a specific position in the list.
    

```python
fruits = ['apple', 'banana', 'date']
fruits.insert(1, 'cherry')
# fruits is now ['apple', 'cherry', 'banana', 'date']
```

1. **remove()**: Removes the first occurrence of a specified value.
    

```python
fruits = ['apple', 'banana', 'cherry']
fruits.remove('banana')
# fruits is now ['apple', 'cherry']
```

1. **pop()**: Removes and returns an element at a specific index. If no index is provided, it removes and returns the last element.
    

```python
fruits = ['apple', 'banana', 'cherry']
removed_fruit = fruits.pop(1)  # Removes 'banana' and returns it
# fruits is now ['apple', 'cherry']
```

1. **index()**: Returns the index of the first occurrence of a specified value.
    

```python
fruits = ['apple', 'banana', 'cherry']
index = fruits.index('banana')  # index is 1
```

1. **count()**: Returns the number of occurrences of a specified value.
    

```python
fruits = ['apple', 'banana', 'cherry', 'banana']
count = fruits.count('banana')  # count is 2
```

1. **sort()**: Sorts the list in ascending order. You can also specify the `reverse` argument to sort in descending order.
    

```python
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
numbers.sort()  # Sorts in ascending order
# numbers is now [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
```

1. **reverse()**: Reverses the order of elements in the list.
    

```python
fruits = ['apple', 'banana', 'cherry']
fruits.reverse()
# fruits is now ['cherry', 'banana', 'apple']
```

1. **copy()**: Creates a shallow copy of the list.
    

```python
fruits = ['apple', 'banana', 'cherry']
fruits_copy = fruits.copy()
# fruits_copy is a new list with the same elements
```

These are some of the essential list methods in Python. Lists are powerful data structures that allow you to store and manipulate collections of items efficiently.

**Dictionaries (dict)**:

* Mutable: Dictionaries consist of key-value pairs and can be modified by adding, removing, or updating key-value pairs.
    
* Use Case: Dictionaries are used to store and retrieve data with a key.
    

Dictionaries in Python are versatile data structures that store key-value pairs. They have several built-in methods that allow you to manipulate and retrieve data from dictionaries. Here are some common dictionary methods with examples:

1. **Creating a Dictionary**:
    
    You can create a dictionary by enclosing key-value pairs in curly braces `{}`.
    
    ```python
    person = {'name': 'John', 'age': 30, 'city': 'New York'}
    ```
    
2. **Accessing Values**:
    
    You can access the values associated with keys using square brackets or the `get()` method.
    
    ```python
    name = person['name']  # name is 'John'
    age = person.get('age')  # age is 30
    ```
    
3. **Adding or Updating Key-Value Pairs**:
    
    You can add new key-value pairs or update existing ones.
    
    ```python
    person['occupation'] = 'Engineer'  # Adds a new key-value pair
    person['age'] = 31  # Updates the value associated with 'age'
    ```
    
4. **Removing Key-Value Pairs**:
    
    You can remove a key-value pair using the `pop()` method or the `del` statement.
    
    ```python
    occupation = person.pop('occupation')  # Removes and returns the value
    del person['city']  # Removes the key-value pair with key 'city'
    ```
    
5. **Checking for Key Existence**:
    
    You can check if a key exists in the dictionary using the `in` operator.
    
    ```python
    has_occupation = 'occupation' in person  # True
    has_height = 'height' in person  # False
    ```
    
6. **Getting Keys and Values**:
    
    You can retrieve all keys or values from a dictionary using the `keys()` and `values()` methods.
    
    ```python
    keys = person.keys()  # Returns a list of keys
    values = person.values()  # Returns a list of values
    ```
    
7. **Getting Key-Value Pairs**:
    
    You can obtain key-value pairs as tuples using the `items()` method.
    
    ```python
    items = person.items()  # Returns a list of key-value pairs
    ```
    
8. **Copying a Dictionary**:
    
    You can create a copy of a dictionary using the `copy()` method or the dictionary constructor.
    
    ```python
    new_person = person.copy()  # Creates a shallow copy
    another_person = dict(person)  # Another way to create a copy
    ```
    
9. **Clearing a Dictionary**:
    
    You can remove all key-value pairs from a dictionary using the `clear()` method.
    
    ```python
    person.clear()  # Removes all key-value pairs in 'person'
    ```
    
10. **Merging Dictionaries**:
    
    You can merge two dictionaries using the `update()` method.
    
    ```python
    person_info = {'occupation': 'Artist', 'height': 175}
    person.update(person_info)  # Merges 'person_info' into 'person'
    ```
    

Dictionaries are widely used for storing and retrieving data based on keys. These methods make it easy to manage and manipulate dictionary data in your Python programs.

**Sets (set)**:

* Mutable: Sets can be modified by adding or removing elements. They automatically remove duplicates.
    
* Use Case: Sets are useful for maintaining a collection of unique items.
    

Sets in Python are unordered collections of unique elements. They have several built-in methods that allow you to manipulate and perform set operations on sets. Here are some common set methods with examples:

1. **Creating a Set**:
    
    You can create a set by enclosing unique elements within curly braces `{}`.
    
    ```python
    fruits = {'apple', 'banana', 'cherry'}
    ```
    
2. **Adding Elements**:
    
    You can add elements to a set using the `add()` method.
    
    ```python
    fruits.add('date')
    ```
    
3. **Removing Elements**:
    
    You can remove elements from a set using the `remove()` or `discard()` method. The difference is that `remove()` raises an error if the element is not present, while `discard()` does not.
    
    ```python
    fruits.remove('banana')
    fruits.discard('grape')  # No error is raised if 'grape' is not in the set
    ```
    
4. **Clearing a Set**:
    
    You can remove all elements from a set using the `clear()` method.
    
    ```python
    fruits.clear()
    ```
    
5. **Copying a Set**:
    
    You can create a copy of a set using the `copy()` method or the set constructor.
    
    ```python
    fruits_copy = fruits.copy()
    another_fruits = set(fruits)
    ```
    
6. **Checking for Existence**:
    
    You can check if an element exists in a set using the `in` operator.
    
    ```python
    has_apple = 'apple' in fruits  # True
    has_orange = 'orange' in fruits  # False
    ```
    
7. **Getting the Set's Length**:
    
    You can find the number of elements in a set using the `len()` function.
    
    ```python
    num_fruits = len(fruits)
    ```
    
8. **Set Operations**:
    
    Sets support various set operations, such as union, intersection, and difference. You can use methods like `union()`, `intersection()`, and `difference()` to perform these operations.
    
    ```python
    set1 = {1, 2, 3}
    set2 = {3, 4, 5}
    union_set = set1.union(set2)  # Union of set1 and set2
    intersection_set = set1.intersection(set2)  # Intersection of set1 and set2
    difference_set = set1.difference(set2)  # Elements in set1 but not in set2
    ```
    
9. **Updating a Set**:
    
    You can use methods like `update()` to add elements from another set to the current set.
    
    ```python
    set1 = {1, 2, 3}
    set2 = {3, 4, 5}
    set1.update(set2)  # Adds elements from set2 to set1
    ```
    
10. **Symmetric Difference**:
    
    The `symmetric_difference()` method returns the elements that are in either of the sets but not in both.
    
    ```python
    set1 = {1, 2, 3}
    set2 = {3, 4, 5}
    symmetric_diff = set1.symmetric_difference(set2)  # {1, 2, 4, 5}
    ```
    

Sets are useful for storing unique elements and performing set operations like union, intersection, and difference. They are often used to work with data that requires uniqueness and mathematical set operations.

**Immutable Data Types:**

**Tuples (tuple)**:

* Immutable: Once created, the elements of a tuple cannot be modified. You can't add, remove, or change elements.
    
* Use Case: Tuples are useful for representing fixed collections of items.
    

**Creating a Tuple**:

You can create a tuple by enclosing elements in parentheses.

* ```plaintext
                fruits = ('apple', 'banana', 'cherry')
    ```
    
* **Accessing Elements**:
    
    You can access individual elements using indexing.
    
* ```plaintext
                first_fruit = fruits[0]  # 'apple'
                second_fruit = fruits[1]  # 'banana'
    ```
    
* **Iterating Over a Tuple**:
    
    You can use a `for` loop to iterate over the elements of a tuple.
    

```plaintext
for fruit in fruits:
    print(fruit)
# Output:
# 'apple'
# 'banana'
# 'cherry'
```

**Strings (str)**:

* Immutable: Strings cannot be modified in-place. When you perform operations on strings, they generate new string objects.
    
* Use Case: Strings are used for text processing and manipulation.
    

**Getting Length**:

You can find the length (number of characters) of a string using the `len()` function.

* ```plaintext
                text = "Hello, World!"
                length = len(text)  # length is 13
    ```
    
* **Converting Case**:
    
    You can convert the case of a string using methods like `lower()`, `upper()`, and `title()`.
    
* ```plaintext
                text = "Hello, World!"
                lowercase = text.lower()  # "hello, world!"
                uppercase = text.upper()  # "HELLO, WORLD!"
                titlecase = text.title()  # "Hello, World!"
    ```
    
* **Replacing Substrings**:
    
    You can replace a substring within a string using the `replace()` method.
    
* ```plaintext
                text = "Hello, World!"
                new_text = text.replace("World", "Python")  # "Hello, Python!"
    ```
    
* **Splitting Strings**:
    
    You can split a string into a list of substrings using the `split()` method.
    

```plaintext
text = "apple, banana, cherry"
fruits = text.split(", ")  # ['apple', 'banana', 'cherry']
```

**Checking Character Types**:

You can check if a string consists of alphabetic characters, digits, etc., using methods like `isalpha()`, `isdigit()`, and so on.

```plaintext
text = "12345"
is_alpha = text.isalpha()  # False
is_digit = text.isdigit()  # True
```

Practice example:

1. Write a python script to check the status of web service by sending HTTP GET request.
    
    ```python
    import requests
    
    # Define the URL of the web service to check
    service_url = 'https://example.com'
    
    # Define the expected HTTP status code (e.g., 200 for a successful response)
    expected_status_code = 200
    
    def check_service_status(url, expected_code):
        try:
            response = requests.get(url)
            if response.status_code == expected_code:
                return True
            else:
                return False
        except requests.exceptions.RequestException:
            return False
    
    if check_service_status(service_url, expected_status_code):
        print("The service at {} is up and running.".format(service_url))
    else:
        print("The service at {} is not responding as expected.".format(service_
    ```
    
2. Write a python script to create 10 file.
    
    ```python
    for i in range(1, 11):
        file_name = "file{}.txt".format(i)
        with open(file_name, 'w') as file:
            pass
    print("10 files created successfully!")
    ```
    

Day 2:

**1\. Arithmetic Operators:**

Arithmetic operators are used to perform mathematical operations in Python.

* Addition `+`: Adds two values.
    
* Subtraction `-`: Subtracts the right operand from the left operand.
    
* Multiplication `*`: Multiplies two values.
    
* Division `/`: Divides the left operand by the right operand, resulting in a floating-point number.
    
* Integer Division `//`: Divides the left operand by the right operand, resulting in an integer.
    
* Modulus `%`: Returns the remainder of the division.
    
* Exponentiation `**`: Raises the left operand to the power of the right operand.
    

Here are some examples:

```plaintext
python
```

```plaintext
# Arithmetic operators example
a = 10
b = 5

addition = a + b  # 10 + 5 = 15
subtraction = a - b  # 10 - 5 = 5
multiplication = a * b  # 10 * 5 = 50
division = a / b  # 10 / 5 = 2.0
integer_division = a // b  # 10 // 5 = 2
modulus = a % b  # 10 % 5 = 0
exponentiation = a ** b  # 10 ** 5 = 100000
```

**Comparison Operators:**

Comparison operators are used to compare two values or expressions and return a Boolean result (True or False).

* Equal `==`: Checks if two values are equal.
    
* Not Equal `!=`: Checks if two values are not equal.
    
* Greater Than `>`: Checks if the left operand is greater than the right operand.
    
* Less Than `<`: Checks if the left operand is less than the right operand.
    
* Greater Than or Equal To `>=`: Checks if the left operand is greater than or equal to the right operand.
    
* Less Than or Equal To `<=`: Checks if the left operand is less than or equal to the right operand.
    

Here are some examples:

```plaintext
python
```

```plaintext
# Comparison operators example
x = 10
y = 20

equal = x == y  # False
not_equal = x != y  # True
greater_than = x > y  # False
less_than = x < y  # True
greater_than_or_equal = x >= y  # False
less_than_or_equal = x <= y  # True
```

**Assignment Operators:**

Assignment operators are used to assign values to variables.

* Assignment `=`: Assigns the value on the right to the variable on the left.
    
* Addition Assignment `+=`: Adds the right operand to the left operand and assigns the result to the left operand.
    
* Subtraction Assignment `-=`: Subtracts the right operand from the left operand and assigns the result to the left operand.
    
* Multiplication Assignment `*=`: Multiplies the left operand by the right operand and assigns the result to the left operand.
    
* Division Assignment `/=`: Divides the left operand by the right operand and assigns the result to the left operand.
    
* Modulus Assignment `%=`: Calculates the remainder of the division and assigns it to the left operand.
    
* Integer Division Assignment `//=`: Divides the left operand by the right operand and assigns the integer result.
    
* Exponentiation Assignment `**=`: Raises the left operand to the power of the right operand and assigns the result.
    

Here are some examples:

```plaintext
python
```

```plaintext
# Assignment operators example
x = 10

x += 5  # x is now 15 (10 + 5)
x -= 3  # x is now 12 (15 - 3)
x *= 2  # x is now 24 (12 * 2)
x /= 4  # x is now 6.0 (24 / 4)
x %= 2  # x is now 0.0 (6.0 % 2)
x //= 3  # x is now 0.0 (0.0 // 3)
x **= 2  # x is now 0.0 (0.0 ** 2)
```

Logical Operator:

**and (Logical AND):** The `and` operator returns `True` if both operands are `True`. If at least one operand is `False`, it returns `False`.

Example:

```plaintext
python
```

* ```plaintext
          x = True
          y = False
          result = x and y  # result is False
    ```
    
* **or (Logical OR):** The `or` operator returns `True` if at least one of the operands is `True`. It returns `False` if both operands are `False`.
    
    Example:
    
    ```plaintext
    python
    ```
    
* ```plaintext
          x = True
          y = False
          result = x or y  # result is True
    ```
    
* **not (Logical NOT):** The `not` operator returns the opposite of the Boolean value. If the operand is `True`, it returns `False`, and if the operand is `False`, it returns `True`.
    
    Example:
    
    ```plaintext
    python
    ```
    

```plaintext
x = True
result = not x  # result is False
```

**Bitwise AND (**`&`): Performs a bitwise AND operation between corresponding bits of two integers. It returns 1 if both bits are 1; otherwise, it returns 0.

Example:

```plaintext
python
```

* ```plaintext
          a = 5  # Binary: 0101
          b = 3  # Binary: 0011
          result = a & b  # Result: 0001 (Decimal: 1)
    ```
    
* **Bitwise OR (**`|`): Performs a bitwise OR operation between corresponding bits of two integers. It returns 1 if at least one of the bits is 1.
    
    Example:
    
    ```plaintext
    python
    ```
    
* ```plaintext
          a = 5  # Binary: 0101
          b = 3  # Binary: 0011
          result = a | b  # Result: 0111 (Decimal: 7)
    ```
    
* **Bitwise XOR (**`^`): Performs a bitwise XOR (exclusive OR) operation between corresponding bits of two integers. It returns 1 if the bits are different; otherwise, it returns 0.
    
    Example:
    
    ```plaintext
    python
    ```
    
* ```plaintext
          a = 5  # Binary: 0101
          b = 3  # Binary: 0011
          result = a ^ b  # Result: 0110 (Decimal: 6)
    ```
    
* **Bitwise NOT (**`~`): Performs a bitwise NOT operation on a single integer. It inverts each bit, turning 0s into 1s and 1s into 0s.
    
    Example:
    
    ```plaintext
    python
    ```
    
* ```plaintext
          a = 5  # Binary: 0101
          result = ~a  # Result: 11111111111111111111111111111010 (Negative binary value)
    ```
    
* **Left Shift (**`<<`): Shifts the bits of an integer to the left by a specified number of positions. It effectively multiplies the integer by 2 raised to the power of the shift amount.
    
    Example:
    
    ```plaintext
    python
    ```
    
* ```plaintext
          a = 5  # Binary: 0101
          result = a << 2  # Result: 20 (Binary: 10100)
    ```
    
* **Right Shift (**`>>`): Shifts the bits of an integer to the right by a specified number of positions. It effectively divides the integer by 2 raised to the power of the shift amount (integer division).
    
    Example:
    
    ```plaintext
    python
    ```
    

```plaintext
a = 20  # Binary: 10100
result = a >> 2  # Result: 5 (Binary: 0101)
```

**is (Identity operator):** The `is` operator returns `True` if both operands reference the same object in memory. It checks for object identity.

Example:

```plaintext
python
```

* ```plaintext
          x = [1, 2, 3]
          y = x  # Both x and y reference the same list object
          result = x is y  # Result: True
    ```
    
* **is not (Negated Identity operator):** The `is not` operator returns `True` if the operands do not reference the same object in memory. It checks for the absence of object identity.
    
    Example:
    
    ```plaintext
    python
    ```
    

```plaintext
a = [1, 2, 3]
b = [1, 2, 3]  # a and b reference different list objects with the same values
result = a is not b  # Result: True
```

**in (Membership operator):** The `in` operator returns `True` if the left operand is found within the sequence or collection on the right.

Example:

```plaintext
python
```

* ```plaintext
          my_list = [1, 2, 3, 4, 5]
          result = 3 in my_list  # Result: True
    ```
    
* **not in (Negated Membership operator):** The `not in` operator returns `True` if the left operand is not found within the sequence or collection on the right.
    
    Example:
    
    ```plaintext
    python
    ```
    

```plaintext
my_string = "Hello, World"
result = 'X' not in my_string  # Result: True
```

Practice Example 3: **Automating AWS Instance Termination**

```python
import boto3

# Replace these with your AWS access key and secret key
aws_access_key = 'YOUR_ACCESS_KEY'
aws_secret_key = 'YOUR_SECRET_KEY'
aws_region = 'us-east-1'  # Update with your AWS region

# Initialize the Boto3 EC2 client
ec2 = boto3.client('ec2', region_name=aws_region, aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key)

# Define the tag criteria for instance termination
tag_filter = [{'Name': 'tag:Environment', 'Values': ['Development']},
              {'Name': 'tag:AutoTerminate', 'Values': ['True']}]

# List instances based on the tag criteria
response = ec2.describe_instances(Filters=tag_filter)

# Terminate instances that match the criteria
for reservation in response['Reservations']:
    for instance in reservation['Instances']:
        instance_id = instance['InstanceId']
        ec2.terminate_instances(InstanceIds=[instance_id])
        print("Terminated instance {}".format(instance_id))
```

Practice Example 4: **Automating Backup and Restore**

```python
import boto3
import os
import shutil

# Replace these with your AWS access key and secret key
aws_access_key = 'YOUR_ACCESS_KEY'
aws_secret_key = 'YOUR_SECRET_KEY'
aws_region = 'us-east-1'  # Update with your AWS region
s3_bucket_name = 'your-s3-bucket-name'

# Source directory to be backed up
source_directory = '/path/to/source_directory'

# Destination directory for the backup
backup_directory = '/path/to/backup_directory'

# Backup: Copy the contents of the source directory to the backup directory
shutil.copytree(source_directory, backup_directory)
print("Backup completed successfully. Files copied to: {}".format(backup_directory))

# Restore: Copy the contents from the backup directory to the source directory
shutil.rmtree(source_directory)
shutil.copytree(backup_directory, source_directory)
print("Restored backup to {}".format(source_directory))

# Initialize the Boto3 S3 client
s3 = boto3.client('s3', region_name=aws_region, aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key)

# Upload the backup to an S3 bucket
s3.upload_file(backup_directory, s3_bucket_name, 'backup.zip')
print("Backup uploaded to S3 bucket: {}".format(s3_bucket_name))
```

Conditional Statement:

Conditional statements in Python allow you to make decisions and control the flow of your program based on certain conditions. Python provides several conditional statements, including `if`, `elif` (short for "else if"), and `else`. These statements are used to execute different blocks of code depending on whether a condition is `True` or `False`.

Here's an overview of Python's conditional statements:

1. `if` Statement:
    
    * The `if` statement is used to test a condition. If the condition is `True`, the code block under the `if` statement is executed.
        
    
    ```python
    if condition:
        # Code to execute if the condition is True
    ```
    
    Example:
    
    ```python
    x = 10
    if x > 5:
        print("x is greater than 5")
    ```
    
2. `elif` Statement:
    
    * The `elif` statement allows you to test multiple conditions sequentially. If the previous `if` or `elif` condition is `False`, it checks the next condition. You can have multiple `elif` clauses.
        
    
    ```python
    if condition1:
        # Code to execute if condition1 is True
    elif condition2:
        # Code to execute if condition2 is True
    elif condition3:
        # Code to execute if condition3 is True
    ```
    
    Example:
    
    ```python
    score = 75
    if score >= 90:
        print("A grade")
    elif score >= 80:
        print("B grade")
    elif score >= 70:
        print("C grade")
    ```
    
3. `else` Statement:
    
    * The `else` statement is used to provide a default code block to execute when none of the preceding conditions are `True`.
        
    
    ```python
    if condition:
        # Code to execute if the condition is True
    else:
        # Code to execute if the condition is False
    ```
    
    Example:
    
    ```python
    age = 17
    if age >= 18:
        print("You are an adult")
    else:
        print("You are a minor")
    ```
    
4. **Nested Conditional Statements:**
    
    * You can nest `if`, `elif`, and `else` statements within each other to handle more complex decision-making scenarios.
        
    
    Example:
    
    ```python
    x = 10
    y = 5
    if x > 5:
        if y > 3:
            print("Both conditions are met")
        else:
            print("x condition is met, but y condition is not")
    else:
        print("x condition is not met")
    ```
    

Conditional statements are fundamental for writing programs that react to different situations and make dynamic choices. They are often used in combination with logical and comparison operators to create more complex conditions.

Practice example : 6

**Script for AWS EC2 Instance Management**

You are responsible for managing AWS EC2 instances. Write a Python script that performs the following tasks:

* Lists all EC2 instances in your AWS account.
    
* Allows you to start, stop, or terminate a specific EC2 instance by providing its instance ID.
    
* Provides the status (running, stopped, terminated) of each EC2 instance.
    

```python
import boto3

# Replace with your AWS access key and secret key
aws_access_key = 'YOUR_ACCESS_KEY'
aws_secret_key = 'YOUR_SECRET_KEY'
aws_region = 'us-east-1'  # Update with your AWS region

# Initialize the Boto3 EC2 client
ec2 = boto3.client('ec2', region_name=aws_region, aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key)

# List all EC2 instances
def list_ec2_instances():
    instances = ec2.describe_instances()
    print("List of EC2 instances:")
    for reservation in instances['Reservations']:
        for instance in reservation['Instances']:
            print("Instance ID: {}, State: {}".format(instance['InstanceId'], instance['State']['Name']))

# Start an EC2 instance by instance ID
def start_ec2_instance(instance_id):
    ec2.start_instances(InstanceIds=[instance_id])
    print("EC2 instance {} started.".format(instance_id))

# Stop an EC2 instance by instance ID
def stop_ec2_instance(instance_id):
    ec2.stop_instances(InstanceIds=[instance_id])
    print("EC2 instance {} stopped.".format(instance_id))

# Terminate an EC2 instance by instance ID
def terminate_ec2_instance(instance_id):
    ec2.terminate_instances(InstanceIds=[instance_id])
    print("EC2 instance {} terminated.".format(instance_id))

# Usage example
if __name__ == '__main__':
    list_ec2_instances()
    
    # Uncomment and modify the following lines as needed
    # start_ec2_instance('your_instance_id')
    # stop_ec2_instance('your_instance_id')
    # terminate_ec2_instance('your_instance_id')
```

In Python, you can use loops to repeatedly execute a block of code. Python supports two main types of loops: `for` loops and `while` loops. These loops are used to perform tasks iteratively, such as iterating over a collection, processing data, or executing code until a certain condition is met.

1. **For Loops:**
    
    A `for` loop is used to iterate over a sequence, which can be a list, tuple, string, dictionary, or any other iterable object. The loop variable takes on each value in the sequence one by one.
    
    **Syntax:**
    
    ```python
    for variable in sequence:
        # Code to execute inside the loop
    ```
    
    Example:
    
    ```python
    fruits = ['apple', 'banana', 'cherry']
    for fruit in fruits:
        print(fruit)
    ```
    
    In this example, the `for` loop iterates over the list of fruits and prints each fruit one by one.
    
2. **While Loops:**
    
    A `while` loop is used to repeatedly execute a block of code as long as a specified condition is `True`. Be cautious with `while` loops to avoid infinite loops.
    
    **Syntax:**
    
    ```python
    while condition:
        # Code to execute inside the loop
    ```
    
    Example:
    
    ```python
    count = 0
    while count < 5:
        print(count)
        count += 1
    ```
    
    In this example, the `while` loop prints numbers from 0 to 4 as long as the `count` is less than 5.
    
3. **Loop Control Statements:**
    
    Python provides loop control statements that allow you to modify the flow of loops. These control statements include:
    
    * `break`: Terminates the loop prematurely.
        
    * `continue`: Skips the rest of the current iteration and proceeds to the next one.
        
    * `else` with a loop: Executes a block of code if the loop completes without encountering a `break` statement.
        
    
    Example with `break`:
    
    ```python
    for number in range(1, 6):
        if number == 3:
            break
        print(number)
    ```
    
    This loop will print numbers 1 and 2, and then terminate when `number` becomes 3.
    
    Example with `continue`:
    
    ```python
    for number in range(1, 6):
        if number == 3:
            continue
        print(number)
    ```
    
    This loop will print numbers 1, 2, 4, and 5, skipping 3.
    

Loops are essential for performing repetitive tasks, processing data, and controlling program flow in Python. Understanding how to use `for` and `while` loops, along with loop control statements, is crucial for writing efficient and effective code.

Practice Example 7: **Autoscaling Script for AWS EC2 Instances**

```python
import boto3

# Replace with your AWS access key and secret key
aws_access_key = 'YOUR_ACCESS_KEY'
aws_secret_key = 'YOUR_SECRET_KEY'
aws_region = 'us-east-1'  # Update with your AWS region
cpu_threshold_high = 80
cpu_threshold_low = 30

# Initialize the Boto3 EC2 client
ec2 = boto3.client('ec2', region_name=aws_region, aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key)

# Function to get CPU utilization of EC2 instances
def get_cpu_utilization(instance_id):
    # Simulated function for getting CPU utilization
    # Replace with actual AWS CloudWatch or monitoring data retrieval
    return 60  # Replace with real CPU utilization

# Function to adjust the number of instances based on CPU utilization
def autoscale_instances():
    instances = ['instance_id_1', 'instance_id_2']  # Replace with your instance IDs
    for instance_id in instances:
        cpu_utilization = get_cpu_utilization(instance_id)
        if cpu_utilization > cpu_threshold_high:
            # Scale up by launching new instances
            # Implement the logic to create new instances here
            print(f"Scaling up instance {instance_id}")
        elif cpu_utilization < cpu_threshold_low:
            # Scale down by terminating instances
            # Implement the logic to terminate instances here
            print(f"Scaling down instance {instance_id}")

# Continuous monitoring and scaling loop
while True:
    autoscale_instances()
    # Add a delay or sleep for a specific interval before checking again
```

Here's the same explanation with examples using the `.format()` method for string formatting:

**Function Definition with .format():**

```python
def my_function(parameter1, parameter2):
    # Function body
    # Code to execute
    result = parameter1 + parameter2
    return result
```

**Function Call with .format():**

```python
result_value = my_function(3, 5)
print("Result: {}".format(result_value))
```

**Default Parameters with .format():**

```python
def greet(name, greeting="Hello"):
    print("{}, {}!".format(greeting, name))

greet("Alice")
greet("Bob", "Hi")
```

**Variable Number of Arguments with .format():**

```python
def print_arguments(*args, **kwargs):
    for arg in args:
        print(arg)
    for key, value in kwargs.items():
        print("{}: {}".format(key, value))

print_arguments(1, "apple", name="Alice", age=25)
```

**Scope of Variables with .format():**

```python
global_variable = "I am global"

def my_function():
    local_variable = "I am local"
    print("Global Variable:", global_variable)
    print("Local Variable:", local_variable)

my_function()
print("Global Variable outside function:", global_variable)
# print("Local Variable outside function:", local_variable)  # This will raise an error
```

In each example, the `.format()` method is used to insert values into the strings. This method allows you to create formatted strings with placeholders `{}`, and the values provided in `.format()` replace these placeholders in the resulting string. It's a common and flexible way to perform string formatting in Python.

Certainly! Let's explore decorators and generators in Python.

### Decorators:

**Definition:** A decorator is a special type of function that can be used to modify the behavior of another function. It allows you to wrap another function and extend or modify its behavior.

**Syntax:**

```python
@decorator
def my_function():
    # Function body
    pass
```

**Example:**

```python
def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()
```

In this example, `my_decorator` is a decorator that wraps the `say_hello` function. It adds behavior before and after the original function call.

### Generators:

**Definition:** A generator is a special type of iterator in Python. It allows you to iterate over a potentially large sequence of data without creating the entire sequence in memory at once. Generators are created using functions with the `yield` keyword.

**Syntax:**

```python
def my_generator():
    # Generator body
    yield some_value
```

**Example:**

```python
def countdown(n):
    while n > 0:
        yield n
        n -= 1

# Using the generator
for value in countdown(5):
    print(value)
```

In this example, `countdown` is a generator function. When called, it returns a generator object. The `yield` statement produces a value each time the generator is iterated over. Generators are memory-efficient, especially for large datasets.

Both decorators and generators are powerful features in Python that enhance code readability, reusability, and efficiency. Decorators are often used for aspect-oriented programming, while generators are handy for dealing with large datasets or creating infinite sequences.
