Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/faker/providers/internet/__init__.py: 27%

244 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1from ipaddress import IPV4LENGTH, IPV6LENGTH, IPv4Network, IPv6Address, IPv6Network 

2from typing import Dict, List, Optional, Tuple 

3 

4from ...decode import unidecode 

5from ...utils.decorators import lowercase, slugify, slugify_unicode 

6from ...utils.distribution import choices_distribution 

7from .. import BaseProvider, ElementsType 

8 

9localized = True 

10 

11 

12class _IPv4Constants: 

13 """ 

14 IPv4 network constants used to group networks into different categories. 

15 Structure derived from `ipaddress._IPv4Constants`. 

16 

17 Excluded network list is updated to comply with current IANA list of 

18 private and reserved networks. 

19 """ 

20 

21 _network_classes: Dict[str, IPv4Network] = { 

22 "a": IPv4Network("0.0.0.0/1"), 

23 "b": IPv4Network("128.0.0.0/2"), 

24 "c": IPv4Network("192.0.0.0/3"), 

25 } 

26 

27 # Three common private networks from class A, B and CIDR 

28 # to generate private addresses from. 

29 _private_networks: List[IPv4Network] = [ 

30 IPv4Network("10.0.0.0/8"), 

31 IPv4Network("172.16.0.0/12"), 

32 IPv4Network("192.168.0.0/16"), 

33 ] 

34 

35 # List of networks from which IP addresses will never be generated, 

36 # includes other private IANA and reserved networks from 

37 # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml 

38 _excluded_networks: List[IPv4Network] = [ 

39 IPv4Network("0.0.0.0/8"), 

40 IPv4Network("100.64.0.0/10"), 

41 IPv4Network("127.0.0.0/8"), # loopback network 

42 IPv4Network("169.254.0.0/16"), # linklocal network 

43 IPv4Network("192.0.0.0/24"), 

44 IPv4Network("192.0.2.0/24"), 

45 IPv4Network("192.31.196.0/24"), 

46 IPv4Network("192.52.193.0/24"), 

47 IPv4Network("192.88.99.0/24"), 

48 IPv4Network("192.175.48.0/24"), 

49 IPv4Network("198.18.0.0/15"), 

50 IPv4Network("198.51.100.0/24"), 

51 IPv4Network("203.0.113.0/24"), 

52 IPv4Network("224.0.0.0/4"), # multicast network 

53 IPv4Network("240.0.0.0/4"), 

54 IPv4Network("255.255.255.255/32"), 

55 ] 

56 

57 

58class Provider(BaseProvider): 

59 safe_domain_names: ElementsType[str] = ("example.org", "example.com", "example.net") 

60 free_email_domains: ElementsType[str] = ("gmail.com", "yahoo.com", "hotmail.com") 

61 tlds: ElementsType[str] = ( 

62 "com", 

63 "com", 

64 "com", 

65 "com", 

66 "com", 

67 "com", 

68 "biz", 

69 "info", 

70 "net", 

71 "org", 

72 ) 

73 hostname_prefixes: ElementsType[str] = ( 

74 "db", 

75 "srv", 

76 "desktop", 

77 "laptop", 

78 "lt", 

79 "email", 

80 "web", 

81 ) 

82 uri_pages: ElementsType[str] = ( 

83 "index", 

84 "home", 

85 "search", 

86 "main", 

87 "post", 

88 "homepage", 

89 "category", 

90 "register", 

91 "login", 

92 "faq", 

93 "about", 

94 "terms", 

95 "privacy", 

96 "author", 

97 ) 

98 uri_paths: ElementsType[str] = ( 

99 "app", 

100 "main", 

101 "wp-content", 

102 "search", 

103 "category", 

104 "tag", 

105 "categories", 

106 "tags", 

107 "blog", 

108 "posts", 

109 "list", 

110 "explore", 

111 ) 

112 uri_extensions: ElementsType[str] = ( 

113 ".html", 

114 ".html", 

115 ".html", 

116 ".htm", 

117 ".htm", 

118 ".php", 

119 ".php", 

120 ".jsp", 

121 ".asp", 

122 ) 

123 http_methods: ElementsType[str] = ( 

124 "GET", 

125 "HEAD", 

126 "POST", 

127 "PUT", 

128 "DELETE", 

129 "CONNECT", 

130 "OPTIONS", 

131 "TRACE", 

132 "PATCH", 

133 ) 

134 

135 user_name_formats: ElementsType[str] = ( 

136 "{{last_name}}.{{first_name}}", 

137 "{{first_name}}.{{last_name}}", 

138 "{{first_name}}##", 

139 "?{{last_name}}", 

140 ) 

141 email_formats: ElementsType[str] = ( 

142 "{{user_name}}@{{domain_name}}", 

143 "{{user_name}}@{{free_email_domain}}", 

144 ) 

145 url_formats: ElementsType[str] = ( 

146 "www.{{domain_name}}/", 

147 "{{domain_name}}/", 

148 ) 

149 uri_formats: ElementsType[str] = ( 

150 "{{url}}", 

151 "{{url}}{{uri_page}}/", 

152 "{{url}}{{uri_page}}{{uri_extension}}", 

153 "{{url}}{{uri_path}}/{{uri_page}}/", 

154 "{{url}}{{uri_path}}/{{uri_page}}{{uri_extension}}", 

155 ) 

156 image_placeholder_services: ElementsType[str] = ( 

157 "https://picsum.photos/{width}/{height}", 

158 "https://dummyimage.com/{width}x{height}", 

159 "https://placekitten.com/{width}/{height}", 

160 "https://placeimg.com/{width}/{height}/any", 

161 ) 

162 

163 replacements: Tuple[Tuple[str, str], ...] = () 

164 

165 def _to_ascii(self, string: str) -> str: 

166 for search, replace in self.replacements: 

167 string = string.replace(search, replace) 

168 

169 string = unidecode(string) 

170 return string 

171 

172 @lowercase 

173 def email(self, safe: bool = True, domain: Optional[str] = None) -> str: 

174 if domain: 

175 email = f"{self.user_name()}@{domain}" 

176 elif safe: 

177 email = f"{self.user_name()}@{self.safe_domain_name()}" 

178 else: 

179 pattern: str = self.random_element(self.email_formats) 

180 email = "".join(self.generator.parse(pattern).split(" ")) 

181 return email 

182 

183 @lowercase 

184 def safe_domain_name(self) -> str: 

185 return self.random_element(self.safe_domain_names) 

186 

187 @lowercase 

188 def safe_email(self) -> str: 

189 return self.user_name() + "@" + self.safe_domain_name() 

190 

191 @lowercase 

192 def free_email(self) -> str: 

193 return self.user_name() + "@" + self.free_email_domain() 

194 

195 @lowercase 

196 def company_email(self) -> str: 

197 return self.user_name() + "@" + self.domain_name() 

198 

199 @lowercase 

200 def free_email_domain(self) -> str: 

201 return self.random_element(self.free_email_domains) 

202 

203 @lowercase 

204 def ascii_email(self) -> str: 

205 pattern: str = self.random_element(self.email_formats) 

206 return self._to_ascii( 

207 "".join(self.generator.parse(pattern).split(" ")), 

208 ) 

209 

210 @lowercase 

211 def ascii_safe_email(self) -> str: 

212 return self._to_ascii(self.user_name() + "@" + self.safe_domain_name()) 

213 

214 @lowercase 

215 def ascii_free_email(self) -> str: 

216 return self._to_ascii( 

217 self.user_name() + "@" + self.free_email_domain(), 

218 ) 

219 

220 @lowercase 

221 def ascii_company_email(self) -> str: 

222 return self._to_ascii( 

223 self.user_name() + "@" + self.domain_name(), 

224 ) 

225 

226 @slugify_unicode 

227 def user_name(self) -> str: 

228 pattern: str = self.random_element(self.user_name_formats) 

229 return self._to_ascii(self.bothify(self.generator.parse(pattern)).lower()) 

230 

231 @lowercase 

232 def hostname(self, levels: int = 1) -> str: 

233 """ 

234 Produce a hostname with specified number of subdomain levels. 

235 

236 >>> hostname() 

237 db-01.nichols-phillips.com 

238 >>> hostname(0) 

239 laptop-56 

240 >>> hostname(2) 

241 web-12.williamson-hopkins.jackson.com 

242 """ 

243 hostname_prefix: str = self.random_element(self.hostname_prefixes) 

244 hostname_prefix_first_level: str = hostname_prefix + "-" + self.numerify("##") 

245 return ( 

246 hostname_prefix_first_level if levels < 1 else hostname_prefix_first_level + "." + self.domain_name(levels) 

247 ) 

248 

249 @lowercase 

250 def domain_name(self, levels: int = 1) -> str: 

251 """ 

252 Produce an Internet domain name with the specified number of 

253 subdomain levels. 

254 

255 >>> domain_name() 

256 nichols-phillips.com 

257 >>> domain_name(2) 

258 williamson-hopkins.jackson.com 

259 """ 

260 if levels < 1: 

261 raise ValueError("levels must be greater than or equal to 1") 

262 if levels == 1: 

263 return self.domain_word() + "." + self.tld() 

264 return self.domain_word() + "." + self.domain_name(levels - 1) 

265 

266 @lowercase 

267 @slugify_unicode 

268 def domain_word(self) -> str: 

269 company: str = self.generator.format("company") 

270 company_elements: List[str] = company.split(" ") 

271 return self._to_ascii(company_elements.pop(0)) 

272 

273 def dga( 

274 self, 

275 year: Optional[int] = None, 

276 month: Optional[int] = None, 

277 day: Optional[int] = None, 

278 tld: Optional[str] = None, 

279 length: Optional[int] = None, 

280 ) -> str: 

281 """Generates a domain name by given date 

282 https://en.wikipedia.org/wiki/Domain_generation_algorithm 

283 

284 :type year: int 

285 :type month: int 

286 :type day: int 

287 :type tld: str 

288 :type length: int 

289 :rtype: str 

290 """ 

291 

292 domain = "" 

293 year = year or self.random_int(min=1, max=9999) 

294 month = month or self.random_int(min=1, max=12) 

295 day = day or self.random_int(min=1, max=30) 

296 tld = tld or self.tld() 

297 length = length or self.random_int(min=2, max=63) 

298 

299 for _ in range(length): 

300 year = ((year ^ 8 * year) >> 11) ^ ((year & 0xFFFFFFF0) << 17) 

301 month = ((month ^ 4 * month) >> 25) ^ 16 * (month & 0xFFFFFFF8) 

302 day = ((day ^ (day << 13)) >> 19) ^ ((day & 0xFFFFFFFE) << 12) 

303 domain += chr(((year ^ month ^ day) % 25) + 97) 

304 

305 return domain + "." + tld 

306 

307 def tld(self) -> str: 

308 return self.random_element(self.tlds) 

309 

310 def http_method(self) -> str: 

311 """Returns random HTTP method 

312 https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods 

313 

314 :rtype: str 

315 """ 

316 

317 return self.random_element(self.http_methods) 

318 

319 def url(self, schemes: Optional[List[str]] = None) -> str: 

320 """ 

321 :param schemes: a list of strings to use as schemes, one will chosen randomly. 

322 If None, it will generate http and https urls. 

323 Passing an empty list will result in schemeless url generation like "://domain.com". 

324 :return: a random url string. 

325 

326 """ 

327 if schemes is None: 

328 schemes = ["http", "https"] 

329 

330 pattern: str = f'{self.random_element(schemes) if schemes else ""}://{self.random_element(self.url_formats)}' 

331 

332 return self.generator.parse(pattern) 

333 

334 def _get_all_networks_and_weights(self, address_class: Optional[str] = None) -> Tuple[List[IPv4Network], List[int]]: 

335 """ 

336 Produces a 2-tuple of valid IPv4 networks and corresponding relative weights 

337 

338 :param address_class: IPv4 address class (a, b, or c) 

339 """ 

340 # If `address_class` has an unexpected value, use the whole IPv4 pool 

341 if address_class in _IPv4Constants._network_classes.keys(): 

342 networks_attr = f"_cached_all_class_{address_class}_networks" 

343 all_networks = [_IPv4Constants._network_classes[address_class]] # type: ignore 

344 else: 

345 networks_attr = "_cached_all_networks" 

346 all_networks = [IPv4Network("0.0.0.0/0")] 

347 

348 # Return cached network and weight data if available 

349 weights_attr = f"{networks_attr}_weights" 

350 if hasattr(self, networks_attr) and hasattr(self, weights_attr): 

351 return getattr(self, networks_attr), getattr(self, weights_attr) 

352 

353 # Otherwise, compute for list of networks (excluding special networks) 

354 all_networks = self._exclude_ipv4_networks( 

355 all_networks, 

356 _IPv4Constants._excluded_networks, 

357 ) 

358 

359 # Then compute for list of corresponding relative weights 

360 weights = [network.num_addresses for network in all_networks] 

361 

362 # Then cache and return results 

363 setattr(self, networks_attr, all_networks) 

364 setattr(self, weights_attr, weights) 

365 return all_networks, weights 

366 

367 def _get_private_networks_and_weights( 

368 self, 

369 address_class: Optional[str] = None, 

370 ) -> Tuple[List[IPv4Network], List[int]]: 

371 """ 

372 Produces an OrderedDict of valid private IPv4 networks and corresponding relative weights 

373 

374 :param address_class: IPv4 address class (a, b, or c) 

375 """ 

376 # If `address_class` has an unexpected value, choose a valid value at random 

377 if not address_class or address_class not in _IPv4Constants._network_classes.keys(): 

378 address_class = self.ipv4_network_class() 

379 

380 # Return cached network and weight data if available for a specific address class 

381 networks_attr = f"_cached_private_class_{address_class}_networks" 

382 weights_attr = f"{networks_attr}_weights" 

383 if hasattr(self, networks_attr) and hasattr(self, weights_attr): 

384 return getattr(self, networks_attr), getattr(self, weights_attr) 

385 

386 # Otherwise, compute for list of private networks (excluding special networks) 

387 supernet = _IPv4Constants._network_classes[address_class] 

388 private_networks = [subnet for subnet in _IPv4Constants._private_networks if subnet.overlaps(supernet)] 

389 private_networks = self._exclude_ipv4_networks( 

390 private_networks, 

391 _IPv4Constants._excluded_networks, 

392 ) 

393 

394 # Then compute for list of corresponding relative weights 

395 weights = [network.num_addresses for network in private_networks] 

396 

397 # Then cache and return results 

398 setattr(self, networks_attr, private_networks) 

399 setattr(self, weights_attr, weights) 

400 return private_networks, weights 

401 

402 def _get_public_networks_and_weights( 

403 self, 

404 address_class: Optional[str] = None, 

405 ) -> Tuple[List[IPv4Network], List[int]]: 

406 """ 

407 Produces a 2-tuple of valid public IPv4 networks and corresponding relative weights 

408 

409 :param address_class: IPv4 address class (a, b, or c) 

410 """ 

411 # If `address_class` has an unexpected value, choose a valid value at random 

412 if address_class not in _IPv4Constants._network_classes.keys(): 

413 address_class = self.ipv4_network_class() 

414 

415 # Return cached network and weight data if available for a specific address class 

416 networks_attr = f"_cached_public_class_{address_class}_networks" 

417 weights_attr = f"{networks_attr}_weights" 

418 if hasattr(self, networks_attr) and hasattr(self, weights_attr): 

419 return getattr(self, networks_attr), getattr(self, weights_attr) 

420 

421 # Otherwise, compute for list of public networks (excluding private and special networks) 

422 public_networks = [_IPv4Constants._network_classes[address_class]] # type: ignore 

423 public_networks = self._exclude_ipv4_networks( 

424 public_networks, 

425 _IPv4Constants._private_networks + _IPv4Constants._excluded_networks, 

426 ) 

427 

428 # Then compute for list of corresponding relative weights 

429 weights = [network.num_addresses for network in public_networks] 

430 

431 # Then cache and return results 

432 setattr(self, networks_attr, public_networks) 

433 setattr(self, weights_attr, weights) 

434 return public_networks, weights 

435 

436 def _random_ipv4_address_from_subnets( 

437 self, 

438 subnets: List[IPv4Network], 

439 weights: Optional[List[int]] = None, 

440 network: bool = False, 

441 ) -> str: 

442 """ 

443 Produces a random IPv4 address or network with a valid CIDR 

444 from within the given subnets using a distribution described 

445 by weights. 

446 

447 :param subnets: List of IPv4Networks to choose from within 

448 :param weights: List of weights corresponding to the individual IPv4Networks 

449 :param network: Return a network address, and not an IP address 

450 :return: 

451 """ 

452 if not subnets: 

453 raise ValueError("No subnets to choose from") 

454 

455 # If the weights argument has an invalid value, default to equal distribution 

456 if ( 

457 isinstance(weights, list) 

458 and len(subnets) == len(weights) 

459 and all(isinstance(w, (float, int)) for w in weights) 

460 ): 

461 subnet = choices_distribution( 

462 subnets, 

463 [float(w) for w in weights], 

464 random=self.generator.random, 

465 length=1, 

466 )[0] 

467 else: 

468 subnet = self.generator.random.choice(subnets) 

469 

470 address = str( 

471 subnet[ 

472 self.generator.random.randint( 

473 0, 

474 subnet.num_addresses - 1, 

475 ) 

476 ], 

477 ) 

478 

479 if network: 

480 address += "/" + str( 

481 self.generator.random.randint( 

482 subnet.prefixlen, 

483 subnet.max_prefixlen, 

484 ) 

485 ) 

486 address = str(IPv4Network(address, strict=False)) 

487 

488 return address 

489 

490 def _exclude_ipv4_networks( 

491 self, networks: List[IPv4Network], networks_to_exclude: List[IPv4Network] 

492 ) -> List[IPv4Network]: 

493 """ 

494 Exclude the list of networks from another list of networks 

495 and return a flat list of new networks. 

496 

497 :param networks: List of IPv4 networks to exclude from 

498 :param networks_to_exclude: List of IPv4 networks to exclude 

499 :returns: Flat list of IPv4 networks 

500 """ 

501 networks_to_exclude.sort(key=lambda x: x.prefixlen) 

502 for network_to_exclude in networks_to_exclude: 

503 

504 def _exclude_ipv4_network(network): 

505 """ 

506 Exclude a single network from another single network 

507 and return a list of networks. Network to exclude 

508 comes from the outer scope. 

509 

510 :param network: Network to exclude from 

511 :returns: Flat list of IPv4 networks after exclusion. 

512 If exclude fails because networks do not 

513 overlap, a single element list with the 

514 orignal network is returned. If it overlaps, 

515 even partially, the network is excluded. 

516 """ 

517 try: 

518 return list(network.address_exclude(network_to_exclude)) 

519 except ValueError: 

520 # If networks overlap partially, `address_exclude` 

521 # will fail, but the network still must not be used 

522 # in generation. 

523 if network.overlaps(network_to_exclude): 

524 return [] 

525 else: 

526 return [network] 

527 

528 nested_networks = list(map(_exclude_ipv4_network, networks)) 

529 networks = [item for nested in nested_networks for item in nested] 

530 

531 return networks 

532 

533 def ipv4_network_class(self) -> str: 

534 """ 

535 Returns a IPv4 network class 'a', 'b' or 'c'. 

536 

537 :returns: IPv4 network class 

538 """ 

539 return self.random_element("abc") 

540 

541 def ipv4( 

542 self, 

543 network: bool = False, 

544 address_class: Optional[str] = None, 

545 private: Optional[str] = None, 

546 ) -> str: 

547 """ 

548 Returns a random IPv4 address or network with a valid CIDR. 

549 

550 :param network: Network address 

551 :param address_class: IPv4 address class (a, b, or c) 

552 :param private: Public or private 

553 :returns: IPv4 

554 """ 

555 if private is True: 

556 return self.ipv4_private(address_class=address_class, network=network) 

557 elif private is False: 

558 return self.ipv4_public(address_class=address_class, network=network) 

559 else: 

560 all_networks, weights = self._get_all_networks_and_weights(address_class=address_class) 

561 return self._random_ipv4_address_from_subnets(all_networks, weights=weights, network=network) 

562 

563 def ipv4_private(self, network: bool = False, address_class: Optional[str] = None) -> str: 

564 """ 

565 Returns a private IPv4. 

566 

567 :param network: Network address 

568 :param address_class: IPv4 address class (a, b, or c) 

569 :returns: Private IPv4 

570 """ 

571 private_networks, weights = self._get_private_networks_and_weights(address_class=address_class) 

572 return self._random_ipv4_address_from_subnets(private_networks, weights=weights, network=network) 

573 

574 def ipv4_public(self, network: bool = False, address_class: Optional[str] = None) -> str: 

575 """ 

576 Returns a public IPv4 excluding private blocks. 

577 

578 :param network: Network address 

579 :param address_class: IPv4 address class (a, b, or c) 

580 :returns: Public IPv4 

581 """ 

582 public_networks, weights = self._get_public_networks_and_weights(address_class=address_class) 

583 return self._random_ipv4_address_from_subnets(public_networks, weights=weights, network=network) 

584 

585 def ipv6(self, network: bool = False) -> str: 

586 """Produce a random IPv6 address or network with a valid CIDR""" 

587 address = str(IPv6Address(self.generator.random.randint(2**IPV4LENGTH, (2**IPV6LENGTH) - 1))) 

588 if network: 

589 address += "/" + str(self.generator.random.randint(0, IPV6LENGTH)) 

590 address = str(IPv6Network(address, strict=False)) 

591 return address 

592 

593 def mac_address(self) -> str: 

594 mac = [self.generator.random.randint(0x00, 0xFF) for _ in range(0, 6)] 

595 return ":".join("%02x" % x for x in mac) 

596 

597 def port_number(self, is_system: bool = False, is_user: bool = False, is_dynamic: bool = False) -> int: 

598 """Returns a network port number 

599 https://tools.ietf.org/html/rfc6335 

600 

601 :param is_system: System or well-known ports 

602 :param is_user: User or registered ports 

603 :param is_dynamic: Dynamic / private / ephemeral ports 

604 :rtype: int 

605 """ 

606 

607 if is_system: 

608 return self.random_int(min=0, max=1023) 

609 elif is_user: 

610 return self.random_int(min=1024, max=49151) 

611 elif is_dynamic: 

612 return self.random_int(min=49152, max=65535) 

613 

614 return self.random_int(min=0, max=65535) 

615 

616 def uri_page(self) -> str: 

617 return self.random_element(self.uri_pages) 

618 

619 def uri_path(self, deep: Optional[int] = None) -> str: 

620 deep = deep if deep else self.generator.random.randint(1, 3) 

621 return "/".join( 

622 self.random_elements(self.uri_paths, length=deep), 

623 ) 

624 

625 def uri_extension(self) -> str: 

626 return self.random_element(self.uri_extensions) 

627 

628 def uri(self) -> str: 

629 pattern: str = self.random_element(self.uri_formats) 

630 return self.generator.parse(pattern) 

631 

632 @slugify 

633 def slug(self, value: Optional[str] = None) -> str: 

634 """Django algorithm""" 

635 if value is None: 

636 value = self.generator.text(20) 

637 return value 

638 

639 def image_url( 

640 self, 

641 width: Optional[int] = None, 

642 height: Optional[int] = None, 

643 placeholder_url: Optional[str] = None, 

644 ) -> str: 

645 """ 

646 Returns URL to placeholder image 

647 Example: http://placehold.it/640x480 

648 

649 :param width: Optional image width 

650 :param height: Optional image height 

651 :param placeholder_url: Optional template string of image URLs from custom 

652 placeholder service. String must contain ``{width}`` and ``{height}`` 

653 placeholders, eg: ``https:/example.com/{width}/{height}``. 

654 :rtype: str 

655 """ 

656 width_ = width or self.random_int(max=1024) 

657 height_ = height or self.random_int(max=1024) 

658 if placeholder_url is None: 

659 placeholder_url = self.random_element(self.image_placeholder_services) 

660 return placeholder_url.format(width=width_, height=height_) 

661 

662 def iana_id(self) -> str: 

663 """Returns IANA Registrar ID 

664 https://www.iana.org/assignments/registrar-ids/registrar-ids.xhtml 

665 

666 :rtype: str 

667 """ 

668 

669 return str(self.random_int(min=1, max=8888888)) 

670 

671 def ripe_id(self) -> str: 

672 """Returns RIPE Organization ID 

673 https://www.ripe.net/manage-ips-and-asns/db/support/organisation-object-in-the-ripe-database 

674 

675 :rtype: str 

676 """ 

677 

678 lex = "?" * self.random_int(min=2, max=4) 

679 num = "%" * self.random_int(min=1, max=5) 

680 return self.bothify(f"ORG-{lex}{num}-RIPE").upper() 

681 

682 def nic_handle(self, suffix: str = "FAKE") -> str: 

683 """Returns NIC Handle ID 

684 https://www.apnic.net/manage-ip/using-whois/guide/person/ 

685 

686 :rtype: str 

687 """ 

688 

689 if len(suffix) < 2: 

690 raise ValueError("suffix length must be greater than or equal to 2") 

691 

692 lex = "?" * self.random_int(min=2, max=4) 

693 num = "%" * self.random_int(min=1, max=5) 

694 return self.bothify(f"{lex}{num}-{suffix}").upper() 

695 

696 def nic_handles(self, count: int = 1, suffix: str = "????") -> List[str]: 

697 """Returns NIC Handle ID list 

698 

699 :rtype: list[str] 

700 """ 

701 

702 return [self.nic_handle(suffix=suffix) for _ in range(count)]