Skip to content

Commit

Permalink
flattened out ValidatedEmail, working on valid tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bnkc committed Aug 5, 2024
1 parent ace8cfa commit 72e1c50
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 96 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
if: "startsWith(github.ref, 'refs/tags/')"
needs: [linux, musllinux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v4
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ except Exception as e:

## Installation

Install EMV from PyPI:
Install emv from PyPI:

```sh
pip install emv
Expand Down Expand Up @@ -53,7 +53,7 @@ except Exception as e:
You can customize the email validation behavior using the `EmailValidator` class:

```python
validator = EmailValidator(
emv = EmailValidator(
allow_smtputf8=True,
allow_empty_local=False,
allow_quoted_local=False,
Expand All @@ -64,7 +64,7 @@ validator = EmailValidator(
email = "example@domain.com"

try:
validated_email = validator.validate_email(email)
validated_email = emv.validate_email(email)
print(validated_email)
except Exception as e:
print(f"Validation error: {e}")
Expand Down Expand Up @@ -113,7 +113,7 @@ except Exception as e:
```python
from emv import EmailValidator

validator = EmailValidator(
emv = EmailValidator(
allow_smtputf8=False,
allow_empty_local=True,
allow_quoted_local=True,
Expand All @@ -124,7 +124,7 @@ validator = EmailValidator(
email = "user@[192.168.1.1]"

try:
validated_email = validator.validate_email(email)
validated_email = emv.validate_email(email)
print(validated_email)
except Exception as e:
print(f"Validation error: {e}")
Expand Down
3 changes: 1 addition & 2 deletions emv/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# This is the public API of emv
from .validator import validate_email, EmailValidator
from .model import ValidatedEmail, ValidatedDomain
from .model import ValidatedEmail

__all__ = [
"validate_email",
"EmailValidator",
"ValidatedEmail",
"ValidatedDomain",
]
68 changes: 42 additions & 26 deletions emv/model.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,58 @@
from typing import Optional
from typing import Optional, Dict, Any


class ValidatedDomain:
def __init__(self, address: Optional[str], name: str):
"""
Represents a validated domain.
class ValidatedEmail:
"""
Represents a validated email address with various components and normalized forms.
Args:
address: The IP address of the domain, if available.
name: The normalized domain name.
"""
self.address = address
self.name = name
Attributes:
original (str): The email address provided to validate_email. If passed as bytes, it will be converted to a string.
normalized (str): The normalized email address should be used instead of the original. It converts IDNA ASCII domain names to Unicode and normalizes both the local part and domain. The normalized address combines the local part and domain name with an '@' sign.
local_part (str): The local part of the email address (the part before the '@' sign) after it has been Unicode normalized.
domain_name (str): The domain part of the email address (the part after the '@' sign) after Unicode normalization.
domain_address (Optional[str]): If the domain part is a domain literal, it will be an IPv4Address or IPv6Address object.
def __repr__(self) -> str:
return f"ValidatedDomain(address={self.address}, name={self.name})"
Methods:
__repr__() -> str:
Returns a string representation of the ValidatedEmail instance, displaying all its attributes.
as_dict() -> Dict[str, Any]:
Returns a dictionary representation of the ValidatedEmail instance. If the domain_address is present, it is converted to a string.
"""

class ValidatedEmail:
def __init__(
self, original: str, normalized: str, local_part: str, domain: ValidatedDomain
self,
original: str,
normalized: str,
local_part: str,
domain_name: str,
domain_address: Optional[str] = None,
):
"""
Represents a validated email.
Args:
original: The original email address.
normalized: The normalized email address.
local_part: The local part of the email.
domain: The validated domain part of the email.
"""
self.original = original
self.normalized = normalized
self.local_part = local_part
self.domain = domain
self.domain_name = domain_name
self.domain_address = domain_address

def __repr__(self) -> str:
return (
f"ValidatedEmail(original={self.original}, normalized={self.normalized}, "
f"local_part={self.local_part}, domain={self.domain})"
f"local_part={self.local_part}, domain_name={self.domain_name}, domain_address={self.domain_address})"
)

def as_dict(self) -> Dict[str, Any]:
d = self.__dict__
if d.get("domain_address"):
d["domain_address"] = repr(d["domain_address"])
return d

def __eq__(self, other) -> bool:
if isinstance(other, ValidatedEmail):
return (
self.original == other.original
and self.normalized == other.normalized
and self.local_part == other.local_part
and self.domain_name == other.domain_name
and self.domain_address == other.domain_address
)
return False
35 changes: 14 additions & 21 deletions emv/validator.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
from typing import Union
from .model import ValidatedEmail, ValidatedDomain
from .model import ValidatedEmail
from emv import _emv


class EmailValidator:
"""
Initializes an EmailValidator object.
Args:
allow_smtputf8: Whether to allow SMTPUTF8.
allow_empty_local: Whether to allow empty local part.
allow_quoted_local: Whether to allow quoted local part.
allow_domain_literal: Whether to allow domain literals.
deliverable_address: Whether to check if the email address is deliverable.
"""

def __init__(
self,
allow_smtputf8: bool = True,
Expand All @@ -12,16 +23,6 @@ def __init__(
allow_domain_literal: bool = False,
deliverable_address: bool = True,
):
"""
Initializes an EmailValidator object.
Args:
allow_smtputf8: Whether to allow SMTPUTF8.
allow_empty_local: Whether to allow empty local part.
allow_quoted_local: Whether to allow quoted local part.
allow_domain_literal: Whether to allow domain literals.
deliverable_address: Whether to check if the email address is deliverable.
"""
self._emv = _emv.EmailValidator(
allow_smtputf8,
allow_empty_local,
Expand All @@ -46,15 +47,12 @@ def validate_email(self, email: Union[str, bytes]) -> ValidatedEmail:
LengthError: If the email length exceeds the maximum allowed length.
"""
validated_email = self._emv.validate_email(email)
domain = ValidatedDomain(
address=validated_email.domain.address,
name=validated_email.domain.name,
)
return ValidatedEmail(
original=validated_email.original,
normalized=validated_email.normalized,
local_part=validated_email.local_part,
domain=domain,
domain_name=validated_email.domain_name,
domain_address=validated_email.domain_address,
)


Expand Down Expand Up @@ -83,11 +81,6 @@ def validate_email(
Returns:
ValidatedEmail: An instance if the email is valid.
Raises:
SyntaxError: If the email syntax is invalid.
DomainLiteralError: If domain literals are not allowed.
LengthError: If the email length exceeds the maximum allowed length.
"""
emv = EmailValidator(
allow_smtputf8,
Expand Down
52 changes: 19 additions & 33 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,6 @@ const CASE_INSENSITIVE_MAILBOX_NAMES: &[&str] = &[
"ftp",
];

#[derive(Clone)]
#[pyclass]
struct ValidatedDomain {
#[pyo3(get)]
address: Option<IpAddr>,
#[pyo3(get)]
name: String,
}

#[pyclass]
struct ValidatedEmail {
#[pyo3(get)]
Expand All @@ -81,7 +72,9 @@ struct ValidatedEmail {
#[pyo3(get)]
local_part: String,
#[pyo3(get)]
domain: ValidatedDomain,
domain_address: Option<IpAddr>,
#[pyo3(get)]
domain_name: String,
}

#[derive(Default)]
Expand Down Expand Up @@ -134,16 +127,17 @@ impl EmailValidator {
validated_local = validated_local.to_lowercase();
}

// Validate the domain
let validated_domain = self._validate_domain(&unvalidated_domain)?;
// Validate the domain name and optional address
let (domain_name, domain_address) = self._validate_domain(&unvalidated_domain)?;

// Construct the normalized email
let normalized = format!("{}@{}", validated_local, validated_domain.name);
let normalized = format!("{}@{}", validated_local, domain_name);

Ok(ValidatedEmail {
original: email.to_string(),
local_part: validated_local,
domain: validated_domain,
domain_name,
domain_address,
normalized,
})
}
Expand Down Expand Up @@ -257,7 +251,7 @@ impl EmailValidator {
))
}

fn _validate_domain(&self, domain: &str) -> PyResult<ValidatedDomain> {
fn _validate_domain(&self, domain: &str) -> PyResult<(String, Option<IpAddr>)> {
// Guard clause if domain is being executed independently
if domain.is_empty() {
return Err(PySyntaxError::new_err(
Expand All @@ -284,10 +278,7 @@ impl EmailValidator {
)
})?;
if let IpAddr::V6(addr) = addr {
return Ok(ValidatedDomain {
name: format!("[IPv6:{}]", addr),
address: Some(IpAddr::V6(addr)),
});
return Ok((format!("[IPv6:{}]", addr), Some(IpAddr::V6(addr))));
}
}

Expand All @@ -296,13 +287,12 @@ impl EmailValidator {
PySyntaxError::new_err("Invalid Domain: The address in brackets following the '@' sign is not a valid IP address.")
})?;

return Ok(ValidatedDomain {
name: match addr {
IpAddr::V4(_) => format!("[{}]", addr),
IpAddr::V6(_) => format!("[IPv6:{}]", addr),
},
address: Some(addr),
});
let name = match addr {
IpAddr::V4(_) => format!("[{}]", addr),
IpAddr::V6(_) => format!("[IPv6:{}]", addr),
};

return Ok((name, Some(addr)));
}

// Check for invalid characters in the domain part
Expand Down Expand Up @@ -403,11 +393,7 @@ impl EmailValidator {
));
}
}

Ok(ValidatedDomain {
name: normalized_domain.to_string(),
address: None,
})
Ok((normalized_domain.to_string(), None))
}
}

Expand Down Expand Up @@ -765,8 +751,8 @@ mod tests {
let result = emv.validate_email(email);
assert!(result.is_ok());
let validated_email = result.unwrap();
assert_eq!(validated_email.domain.name, expected_domain);
assert_eq!(validated_email.domain.address, expected_ip);
assert_eq!(validated_email.domain_name, expected_domain);
assert_eq!(validated_email.domain_address, expected_ip);
}

#[rstest]
Expand Down
Loading

0 comments on commit 72e1c50

Please sign in to comment.