<commit_before>import secrets
from argparse import FileType
from pathlib import Path

from django.contrib.gis.db.models.functions import Distance as DistanceFunction
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from phonenumber_field.phonenumber import PhoneNumber
from phonenumbers import number_type, PhoneNumberType
from tqdm import tqdm

from agir.lib.management_utils import (
    datetime_argument,
    distance_argument,
    event_argument,
    coordinates_argument,
    departement_argument,
    region_argument,
    segment_argument,
)
from agir.lib.sms import compute_sms_length_information, send_bulk_sms, SMSSendException
from agir.people.models import Person


def drop_duplicate_numbers(it, n=None):
    s = set()

    for number, distance in it:
        if number not in s:
            s.add(number)
            yield number, distance

        if n and len(s) >= n:
            return


class Command(BaseCommand):
    help = "\n".join(
        map(
            str.strip,
            """
        Permet d'envoyer un SMS à un certain nombre de personnes. Pour envoyer un SMS, vous devez sélectionner :
            - L'option -e pour indiquer l'ID d'un événement, puis -n ou -d pour le nombre de personnes, ou la distance
            - l'option -D puis le code à deux chiffres du département
            - l'option -R puis le code à deux chiffres ou le nom de la région
            - l'option -c puis les coordonnées géographiques lon,lat avec -n ou -d pour indiquer nombre ou distance
    """.splitlines(),
        )
    )

    def add_arguments(self, parser):
        parser.add_argument("-e", "--event", type=event_argument)
        parser.add_argument("-c", "--coordinates", type=coordinates_argument)
        parser.add_argument("-n", "--number", type=int)
        parser.add_argument("-d", "--distance", type=distance_argument)
        parser.add_argument("-D", "--departement", type=departement_argument)
        parser.add_argument("-R", "--region", type=region_argument)
        parser.add_argument("-S", "--segment", type=segment_argument)
        parser.add_argument("-a", "--at", type=datetime_argument)
        parser.add_argument("-T", "--exclude-telegram", action="store_true")
        parser.add_argument("-s", "--sentfile", type=FileType(mode="r"))

    def can_send(self, phone):
        return (
            isinstance(phone, PhoneNumber)
            and phone.is_valid()
            and number_type(phone)
            in [PhoneNumberType.MOBILE, PhoneNumberType.FIXED_LINE_OR_MOBILE,]
        )

    def write_numbers(self, path, numbers):
        with open(path, "w") as f:
            f.write("\n".join(numbers))

    def read_numbers(self, file):
        return set(PhoneNumber.from_string(n) for n in file.read().split("\n"))

    def handle(
        self,
        event,
        coordinates,
        number,
        distance,
        departement,
        region,
        segment,
        sentfile,
        at,
        exclude_telegram,
        **options,
    ):
        if (
            sum(
                0 if arg is None else 1
                for arg in [event, coordinates, departement, region, segment]
            )
            != 1
        ):
            raise CommandError(
                "Vous devez indiquer soit un événemnet, soit des coordonnées, soit un département, une région ou un segment."
            )

        if (event or coordinates) and number is None and distance is None:
            raise CommandError(
                "Vous devez indiquer soit un nombre de personnes, soit une distance max."
            )

        if event:  # we generate coordinates from event
            if event.coordinates is None:
                raise CommandError(
                    "Cet événement n'a pas de coordonnées géographiques."
                )

            coordinates = event.coordinates

            self.stdout.write(f"Évènement : {event.name}")
            self.stdout.write(event.short_location())
            self.stdout.write(
                event.start_time.astimezone(timezone.get_default_timezone()).strftime(
                    "Le %d/%m/%Y à %H:%M"
                )
            )
            self.stdout.write("\n")  # ligne vide

        if coordinates:  # two case : central point coordinates or any other
            ps = (
                Person.objects.filter(subscribed_sms=True)
                .exclude(contact_phone="")
                .annotate(distance=DistanceFunction("coordinates", coordinates))
                .order_by("distance")
            )

            if distance is not None:
                ps = ps.filter(distance__lte=distance)

            if exclude_telegram:
                ps = ps.exclude(meta__has_telegram=True)

            res = list(
                drop_duplicate_numbers(
                    (
                        (p.contact_phone, p.distance)
                        for p in ps.iterator()
                        if self.can_send(p.contact_phone)
                    ),
                    number,
                )
            )

            numbers = [n for n, _ in res]
            max_distance = res[-1][1]
            self.stdout.write(f"Distance maximale : {max_distance}")
        else:
            if segment:
                ps = (
                    segment.get_subscribers_queryset()
                    .exclude(contact_phone="")
                    .distinct()
                )
                self.stdout.write(f"Segment : {segment.name}")
            else:
                ps = Person.objects.filter(
                    region or departement, subscribed_sms=True
                ).exclude(contact_phone="")

            if exclude_telegram:
                ps = ps.exclude(meta__has_telegram=True)

            numbers = set(
                p.contact_phone for p in ps.iterator() if self.can_send(p.contact_phone)
            )

        self.stdout.write(f"Nombre de numéros : {len(numbers)}")

        if sentfile is not None:
            sent_numbers = self.read_numbers(sentfile)
            numbers.difference_update(sent_numbers)
            self.stdout.write(
                f"{len(numbers)} après prise en compte des numéros déjà envoyés."
            )

        self.stdout.write("\n")  # empty line<commit_msg>feat: export numbers in send_sms<commit_after>import secrets
from argparse import FileType
from pathlib import Path

from django.contrib.gis.db.models.functions import Distance as DistanceFunction
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from phonenumber_field.phonenumber import PhoneNumber
from phonenumbers import number_type, PhoneNumberType
from tqdm import tqdm

from agir.lib.management_utils import (
    datetime_argument,
    distance_argument,
    event_argument,
    coordinates_argument,
    departement_argument,
    region_argument,
    segment_argument,
)
from agir.lib.sms import compute_sms_length_information, send_bulk_sms, SMSSendException
from agir.people.models import Person


def drop_duplicate_numbers(it, n=None):
    s = set()

    for number, distance in it:
        if number not in s:
            s.add(number)
            yield number, distance

        if n and len(s) >= n:
            return


class Command(BaseCommand):
    help = "\n".join(
        map(
            str.strip,
            """
        Permet d'envoyer un SMS à un certain nombre de personnes. Pour envoyer un SMS, vous devez sélectionner :
            - L'option -e pour indiquer l'ID d'un événement, puis -n ou -d pour le nombre de personnes, ou la distance
            - l'option -D puis le code à deux chiffres du département
            - l'option -R puis le code à deux chiffres ou le nom de la région
            - l'option -c puis les coordonnées géographiques lon,lat avec -n ou -d pour indiquer nombre ou distance
    """.splitlines(),
        )
    )

    def add_arguments(self, parser):
        parser.add_argument("-e", "--event", type=event_argument)
        parser.add_argument("-c", "--coordinates", type=coordinates_argument)
        parser.add_argument("-n", "--number", type=int)
        parser.add_argument("-d", "--distance", type=distance_argument)
        parser.add_argument("-D", "--departement", type=departement_argument)
        parser.add_argument("-R", "--region", type=region_argument)
        parser.add_argument("-S", "--segment", type=segment_argument)
        parser.add_argument("-a", "--at", type=datetime_argument)
        parser.add_argument("-T", "--exclude-telegram", action="store_true")
        parser.add_argument("-s", "--sentfile", type=FileType(mode="r"))
        parser.add_argument("-E", "--export-file", type=FileType(mode="w"))

    def can_send(self, phone):
        return (
            isinstance(phone, PhoneNumber)
            and phone.is_valid()
            and number_type(phone)
            in [PhoneNumberType.MOBILE, PhoneNumberType.FIXED_LINE_OR_MOBILE,]
        )

    def write_numbers(self, path, numbers):
        with open(path, "w") as f:
            f.write("\n".join(numbers))

    def read_numbers(self, file):
        return set(PhoneNumber.from_string(n) for n in file.read().split("\n"))

    def handle(
        self,
        event,
        coordinates,
        number,
        distance,
        departement,
        region,
        segment,
        sentfile,
        at,
        exclude_telegram,
        export_file,
        **options,
    ):
        if (
            sum(
                0 if arg is None else 1
                for arg in [event, coordinates, departement, region, segment]
            )
            != 1
        ):
            raise CommandError(
                "Vous devez indiquer soit un événemnet, soit des coordonnées, soit un département, une région ou un segment."
            )

        if (event or coordinates) and number is None and distance is None:
            raise CommandError(
                "Vous devez indiquer soit un nombre de personnes, soit une distance max."
            )

        if event:  # we generate coordinates from event
            if event.coordinates is None:
                raise CommandError(
                    "Cet événement n'a pas de coordonnées géographiques."
                )

            coordinates = event.coordinates

            self.stdout.write(f"Évènement : {event.name}")
            self.stdout.write(event.short_location())
            self.stdout.write(
                event.start_time.astimezone(timezone.get_default_timezone()).strftime(
                    "Le %d/%m/%Y à %H:%M"
                )
            )
            self.stdout.write("\n")  # ligne vide

        if coordinates:  # two case : central point coordinates or any other
            ps = (
                Person.objects.filter(subscribed_sms=True)
                .exclude(contact_phone="")
                .annotate(distance=DistanceFunction("coordinates", coordinates))
                .order_by("distance")
            )

            if distance is not None:
                ps = ps.filter(distance__lte=distance)

            if exclude_telegram:
                ps = ps.exclude(meta__has_telegram=True)

            res = list(
                drop_duplicate_numbers(
                    (
                        (p.contact_phone, p.distance)
                        for p in ps.iterator()
                        if self.can_send(p.contact_phone)
                    ),
                    number,
                )
            )

            numbers = [n for n, _ in res]
            max_distance = res[-1][1]
            self.stdout.write(f"Distance maximale : {max_distance}")
        else:
            if segment:
                ps = (
                    segment.get_subscribers_queryset()
                    .exclude(contact_phone="")
                    .distinct()
                )
                self.stdout.write(f"Segment : {segment.name}")
            else:
                ps = Person.objects.filter(
                    region or departement, subscribed_sms=True
                ).exclude(contact_phone="")

            if exclude_telegram:
                ps = ps.exclude(meta__has_telegram=True)

            numbers = set(
                p.contact_phone for p in ps.iterator() if self.can_send(p.contact_phone)
            )

        self.stdout.write(f"Nombre de numéros : {len(numbers)}")

        if export_file is not None:
            export_file.write("\n".join(numbers))
            return

        if sentfile is not None:
            sent_numbers = self.read_numbers(sentfile)
            numbers.difference_update(sent_numbers)
            self.stdout.write(
                f"{len(numbers)} après prise en compte des numéros déjà envoyés."
            )

        self.stdout.write("\n")  # empty line
{'fraction_non_alphanumeric': 0.09395920909232446, 'fraction_numerical': 0.0007784524365561264, 'mean_word_length': 1.7970825168735032, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2878847', 'n_tokens_mistral': 3586, 'n_tokens_neox': 3473, 'n_words': 908}