Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 44 additions & 43 deletions datamimic_ce/domains/common/generators/address_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,38 @@


import random
from dataclasses import dataclass

from datamimic_ce.domains.common.generators.city_generator import CityGenerator
from datamimic_ce.domains.common.generators.country_generator import CountryGenerator
from datamimic_ce.domains.common.generators.region_groups import REGION_GROUPS
from datamimic_ce.domains.common.literal_generators.company_name_generator import CompanyNameGenerator
from datamimic_ce.domains.common.literal_generators.phone_number_generator import PhoneNumberGenerator
from datamimic_ce.domains.common.literal_generators.street_name_generator import StreetNameGenerator
from datamimic_ce.domains.domain_core.base_domain_generator import DatasetAwareDomainGenerator


@dataclass(frozen=True)
class AddressRow:
"""The concrete country and sub-generators resolved for ONE address (one row)."""

dataset: str
city_generator: CityGenerator
country_generator: CountryGenerator
phone_number_generator: PhoneNumberGenerator
street_name_generator: StreetNameGenerator


class AddressGenerator(DatasetAwareDomainGenerator):
"""Generator for address data.

This class generates random address data using the data from datasets.

``dataset`` may also be a region-group alias (e.g. ``"europe"``, see
:data:`REGION_GROUPS`) - each row (see :meth:`resolve_row`) then independently draws a
concrete country from the group, since a single ``AddressGenerator`` instance is reused
across every row of a run (resolving the country once here in ``__init__`` would give
every row in the run the same one, not variety).
"""

def __init__(
Expand All @@ -29,45 +48,36 @@ def __init__(
"""Initialize the AddressGenerator.

Args:
dataset: The dataset to use for generating addresses.
dataset: The dataset to use for generating addresses, or a region-group alias.
rng: Optional seeded random instance for deterministic output.
"""
super().__init__(dataset=dataset, rng=rng)

# Init sub-generators
self._city_generator = CityGenerator(dataset=self._dataset, rng=self._derive_rng())
self._country_generator = CountryGenerator(dataset=self._dataset, rng=self._derive_rng())
self._phone_number_generator = PhoneNumberGenerator(dataset=self._dataset, rng=self._derive_rng())
self._company_name_generator = CompanyNameGenerator(rng=self._derive_rng())
# Lazy initialization of street name generator
self._street_name_generator = StreetNameGenerator(dataset=self._dataset, rng=self._derive_rng())

@property
def city_generator(self) -> CityGenerator:
"""Get the city generator.

Returns:
The city generator.
"""
return self._city_generator

@property
def country_generator(self) -> CountryGenerator:
"""Get the country generator.

Returns:
The country generator.
"""
return self._country_generator

@property
def phone_number_generator(self) -> PhoneNumberGenerator:
"""Get the phone number generator.

Returns:
The phone number generator.
"""
return self._phone_number_generator
self._region_codes = REGION_GROUPS.get(self._dataset)
self._row_cache: dict[str, AddressRow] = {}
if self._region_codes is None:
# Single concrete dataset (the common case): build once, same as before this class
# supported region groups - identical behavior, zero per-row overhead.
self._row_cache[self._dataset] = self._build_row(self._dataset)

def _build_row(self, dataset: str) -> AddressRow:
return AddressRow(
dataset=dataset,
city_generator=CityGenerator(dataset=dataset, rng=self._derive_rng()),
country_generator=CountryGenerator(dataset=dataset, rng=self._derive_rng()),
phone_number_generator=PhoneNumberGenerator(dataset=dataset, rng=self._derive_rng()),
street_name_generator=StreetNameGenerator(dataset=dataset, rng=self._derive_rng()),
)

def resolve_row(self) -> AddressRow:
"""The concrete dataset and sub-generators for ONE address. A single concrete dataset
always resolves to the same (cached) row. A region group draws a fresh country each call,
reusing a previously-built row for a country drawn again."""
dataset = self._rng.choice(self._region_codes) if self._region_codes is not None else self._dataset
if dataset not in self._row_cache:
self._row_cache[dataset] = self._build_row(dataset)
return self._row_cache[dataset]

@property
def company_name_generator(self) -> CompanyNameGenerator:
Expand All @@ -77,12 +87,3 @@ def company_name_generator(self) -> CompanyNameGenerator:
The company name generator.
"""
return self._company_name_generator

@property
def street_name_generator(self) -> StreetNameGenerator:
"""Get the street name generator.

Returns:
The street name generator.
"""
return self._street_name_generator
56 changes: 56 additions & 0 deletions datamimic_ce/domains/common/generators/region_groups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# DATAMIMIC
# Copyright (c) 2023-2025 Rapiddweller Asia Co., Ltd.
# This software is licensed under the MIT License.
# See LICENSE file for the full text of the license.
# For questions and support, contact: info@rapiddweller.com

"""Dataset region-group aliases: a single ``dataset=`` value that expands to a pool of concrete
ISO country codes, each of which already has its own city/country/street data file in this repo.
No new locale data - purely a grouping over what already exists."""

# European countries with an existing city_{CC}.csv (and matching country/street files) in this
# repo - geographic Europe, EU/EFTA/UK plus the Balkans and microstates. Deliberately excludes
# transcontinental/ambiguous cases (e.g. Turkey) to keep the grouping unsurprising.
REGION_GROUPS: dict[str, tuple[str, ...]] = {
"EUROPE": (
"AD",
"AL",
"AT",
"BA",
"BE",
"BG",
"CH",
"CY",
"CZ",
"DE",
"DK",
"EE",
"ES",
"FI",
"FR",
"GB",
"GR",
"HR",
"HU",
"IE",
"IS",
"IT",
"LI",
"LT",
"LU",
"LV",
"MC",
"NL",
"NO",
"PL",
"PT",
"RO",
"RU",
"SE",
"SI",
"SK",
"SM",
"UA",
"VA",
),
}
22 changes: 13 additions & 9 deletions datamimic_ce/domains/common/models/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ class Address(BaseEntity):
def __init__(self, address_generator: AddressGenerator):
super().__init__()
self._address_generator = address_generator
# Resolved ONCE per address (region-group datasets draw a fresh country per address;
# a single concrete dataset always resolves to the same one) - every property below
# reads through this so city/phone/street/country all agree on the same country.
self._row = address_generator.resolve_row()

@property
@property_cache
def street(self) -> str:
return self._address_generator.street_name_generator.generate()
return self._row.street_name_generator.generate()

@property
@property_cache
Expand All @@ -41,7 +45,7 @@ def house_number(self) -> str:
@property
@property_cache
def city_data(self) -> dict[str, Any]:
return self._address_generator.city_generator.get_random_city()
return self._row.city_generator.get_random_city()

@property
@property_cache
Expand Down Expand Up @@ -71,7 +75,7 @@ def zip_code(self) -> str:
@property
@property_cache
def country_data(self) -> tuple[str, ...]:
return self._address_generator.country_generator.get_country_by_iso_code(self.country_code)
return self._row.country_generator.get_country_by_iso_code(self.country_code)

@property
@property_cache
Expand All @@ -81,32 +85,32 @@ def country(self) -> str:
@property
@property_cache
def country_code(self) -> str:
return self._address_generator.dataset
return self._row.dataset

@property
@property_cache
def phone(self) -> str:
return self._address_generator.phone_number_generator.generate()
return self._row.phone_number_generator.generate()

@property
@property_cache
def mobile_phone(self) -> str:
return self._address_generator.phone_number_generator.generate()
return self._row.phone_number_generator.generate()

@property
@property_cache
def office_phone(self) -> str:
return self._address_generator.phone_number_generator.generate()
return self._row.phone_number_generator.generate()

@property
@property_cache
def private_phone(self) -> str:
return self._address_generator.phone_number_generator.generate()
return self._row.phone_number_generator.generate()

@property
@property_cache
def fax(self) -> str:
return self._address_generator.phone_number_generator.generate()
return self._row.phone_number_generator.generate()

@property
@property_cache
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<setup rngSeed="11" multiprocessing="False">
<generate name="europe_rows" count="40" target="JSON">
<variable name="addr" entity="Address" dataset="europe"/>
<key name="country_code" script="addr.country_code"/>
<key name="city_country_code" script="addr.city_data['country_code']"/>
<key name="street" script="addr.street"/>
<key name="phone" script="addr.phone"/>
</generate>

<!-- Single-country dataset must be completely unaffected (regression). -->
<generate name="de_rows" count="5" target="JSON">
<variable name="addr" entity="Address" dataset="DE"/>
<key name="country_code" script="addr.country_code"/>
</generate>
</setup>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# DATAMIMIC
# Copyright (c) 2023-2025 Rapiddweller Asia Co., Ltd.
# This software is licensed under the MIT License.
# See LICENSE file for the full text of the license.
# For questions and support, contact: info@rapiddweller.com

"""dataset='europe' is a region-group alias: each row independently draws a concrete European
country - AddressGenerator instances are reused across every row of a run (see
generate_worker.py/base_domain_service.py), so resolving the country once in __init__ would give
every row in the run the SAME country, not variety. The draw has to happen per row."""

from pathlib import Path

from datamimic_ce.data_mimic_test import DataMimicTest
from datamimic_ce.domains.common.generators.region_groups import REGION_GROUPS

_dir = Path(__file__).resolve().parent


def _run():
engine = DataMimicTest(test_dir=_dir, filename="address_region_group.xml", capture_test_result=True)
engine.test_with_timer()
return engine.capture_result()


def test_europe_draws_vary_per_row_and_stay_in_the_group():
result = _run()
codes = {row["country_code"] for row in result["europe_rows"]}
assert len(codes) > 1, "dataset='europe' produced the same country for every row"
assert codes <= set(REGION_GROUPS["EUROPE"])


def test_europe_sub_generators_are_consistent_with_the_drawn_country():
"""city/phone/street must reflect the SAME country_code that was drawn for that row, not a
stale one left over from AddressGenerator's own (shared, reused) fixed dataset."""
result = _run()
for row in result["europe_rows"]:
assert row["city_country_code"] == row["country_code"]
assert row["street"]
assert row["phone"]


def test_single_country_dataset_is_unaffected():
result = _run()
assert {row["country_code"] for row in result["de_rows"]} == {"DE"}


def test_europe_is_deterministic_under_a_seed():
r1, r2 = _run(), _run()
assert [row["country_code"] for row in r1["europe_rows"]] == [row["country_code"] for row in r2["europe_rows"]]
15 changes: 11 additions & 4 deletions tests_ce/unit_tests/test_generator/test_person_gender_alignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import pytest

from datamimic_ce.domains.common.demographics.sampler import DemographicSample
from datamimic_ce.domains.common.generators.address_generator import AddressRow
from datamimic_ce.domains.common.generators.person_generator import PersonGenerator
from datamimic_ce.domains.common.models.address import Address
from datamimic_ce.domains.common.models.person import Person
Expand Down Expand Up @@ -135,11 +136,17 @@ class _SentinelAddressGenerator:
def __init__(self):
self.dataset = "DE"
self.rng = Random(11)
self.street_name_generator = _SentinelGenerator("Teststreet")
self.city_generator = _StaticCityGenerator()
self.country_generator = _StaticCountryGenerator()
self.phone_number_generator = _SentinelGenerator("+49-30-123456")
self.company_name_generator = _SentinelGenerator("Example GmbH")
self._row = AddressRow(
dataset="DE",
city_generator=_StaticCityGenerator(),
country_generator=_StaticCountryGenerator(),
phone_number_generator=_SentinelGenerator("+49-30-123456"),
street_name_generator=_SentinelGenerator("Teststreet"),
)

def resolve_row(self) -> AddressRow:
return self._row


class _StaticCityGenerator:
Expand Down