Esempio n. 1
0
    def get_port_mab(self,
                     ctx,
                     switch: Switch = None,
                     port: Port = None) -> bool:
        """
        Retrieve whether MAB is active on a port.

        :raise PortNotFound
        """

        LOG.debug("switch_network_manager_get_port_mab_called",
                  extra=log_extra(port))

        if switch is None:
            raise NetworkManagerReadError(
                "SNMP read error: switch object was None")
        if port is None:
            raise NetworkManagerReadError(
                "SNMP read error: port object was None")

        try:
            return get_SNMP_value(switch.community, switch.ip_v4,
                                  'CISCO-MAC-AUTH-BYPASS-MIB',
                                  'cmabIfAuthEnabled', port.switch_info.oid)
        except NetworkManagerReadError:
            raise
Esempio n. 2
0
    def search_transaction_by(self, ctx, limit=DEFAULT_LIMIT, offset=DEFAULT_OFFSET, transaction_id=None,
                              account_id=None, terms=None) \
            -> (List[Transaction], int):
        LOG.debug("sql_transaction_repository_search_called",
                  extra=log_extra(ctx))
        s = ctx.get(CTX_SQL_SESSION)

        account_source = aliased(Account)
        account_destination = aliased(Account)

        q = s.query(SQLTransaction)
        q = q.join(account_source, account_source.id == SQLTransaction.dst)
        q = q.join(account_destination,
                   account_destination.id == SQLTransaction.src)

        if transaction_id:
            q = q.filter(SQLTransaction.id == transaction_id)
        if terms:
            q = q.filter((SQLTransaction.name.contains(terms)))
        if account_id:
            q = q.filter((SQLTransaction.src == account_id)
                         | (SQLTransaction.dst == account_id))

        count = q.count()
        q = q.order_by(SQLTransaction.timestamp.asc())
        q = q.offset(offset)
        q = q.limit(limit)
        r = q.all()

        return list(map(_map_transaction_sql_to_entity, r)), count
Esempio n. 3
0
    def update_partially(self, ctx, username,
                         mutation_request: PartialMutationRequest) -> None:
        """
        User story: As an admin, I can modify some of the fields of a profile, so that I can update the information of
        a member.

        :raise MemberNotFound
        """
        # Perform all the checks on the validity of the data in the mutation request.
        mutation_request.validate()

        # Create a dict with all the changed field. If a field is None it will not be put in the dict, and the
        # field will not be updated.
        fields_to_update = asdict(mutation_request)
        fields_to_update = {
            k: v
            for k, v in fields_to_update.items() if v is not None
        }

        self.member_repository.update_member(ctx, username, **fields_to_update)

        # Log action.
        LOG.info('member_partial_update',
                 extra=log_extra(ctx,
                                 username=username,
                                 mutation=json.dumps(fields_to_update,
                                                     sort_keys=True,
                                                     default=str)))
Esempio n. 4
0
    def _init_board_tab(self):
        menus = [
            MenuItem('主  音', ALL_NOTES),
            MenuItem('类  型', list(all_scale.keys())),
            MenuItem('调  弦', list(TUNINGS.keys())),
            MenuItem('符  号', ['音名', '数字']),
        ]
        menu = MenuPage(self.hei - 3,
                        self.wid // sum(self.ration) * self.ration[0],
                        0,
                        0,
                        items=menus,
                        chosen=True)

        LOG.info(f'the menu of chord tab created. winlist:{menu.winlist}')

        boards = self.get_board(*[item.cur for item in menus])
        page = BoardPage(self.hei - 3,
                         self.wid // sum(self.ration) * self.ration[1],
                         0,
                         self.wid // sum(self.ration) * self.ration[0],
                         items=boards,
                         chosen=False)

        LOG.info(f'the page of chord tab created. winlist:{page.winlist}')
        return {
            'name': 'board',
            'menu': menu,
            'page': page,
            'cur_page': 'menu'
        }
Esempio n. 5
0
    def add_member_payment_record(self, ctx, amount_in_cents: int, title: str, member_username: str,
                                  payment_method: str) -> None:
        LOG.debug("sql_money_repository_add_payment_record",
                  extra=log_extra(ctx, amount=amount_in_cents / 100, title=title, username=member_username,
                                  payment_method=payment_method))
        now = datetime.now()
        s = ctx.get(CTX_SQL_SESSION)
        admin = ctx.get(CTX_ADMIN)

        admin_sql = s.query(Utilisateur).filter(Utilisateur.login == admin.login).one_or_none()
        if admin_sql is None:
            raise InvalidAdmin()

        member_sql = s.query(Adherent).filter(Adherent.login == member_username).one_or_none()
        if member_sql is None:
            raise MemberNotFoundError(member_username)

        payment_method_db = PAYMENT_METHOD_TO_DB.get(payment_method)
        if payment_method_db is None:
            raise UnknownPaymentMethod()

        e = Ecriture(
            intitule=title,
            montant=amount_in_cents / 100,
            moyen=payment_method,
            date=now,
            compte_id=1,  # Compte pour les paiement des adherents.
            created_at=now,
            updated_at=now,
            utilisateur=admin_sql,
            adherent=member_sql,
        )
        s.add(e)
Esempio n. 6
0
    def search(self,
               ctx,
               limit=DEFAULT_LIMIT,
               offset=DEFAULT_OFFSET,
               payment_method_id=None,
               terms=None) -> (List[PaymentMethod], int):
        """
        Search payment methods in the database.
        """

        if limit < 0:
            raise IntMustBePositive('limit')

        if offset < 0:
            raise IntMustBePositive('offset')

        result, count = self.payment_method_repository.search_payment_method_by(
            ctx,
            limit=limit,
            offset=offset,
            payment_method_id=payment_method_id,
            terms=terms)
        LOG.info("payment_method_search",
                 extra=log_extra(ctx,
                                 payment_method_id=payment_method_id,
                                 terms=terms))

        return result, count
Esempio n. 7
0
 def loop(self):
     self.draw()
     while True:
         k = self.getch()
         k = chr(k)
         if k == 'q':
             LOG.info('quit')
             self.exit()
             break
         elif k == '`':
             LOG.info('change tab')
             self.change_tab()
             self.draw()
         elif k == '\t':
             LOG.info('change page')
             self.change_page()
             self.draw()
         elif k == 'r':
             LOG.info('refresh')
             menu = self.cur_tab['menu'].get_menu()
             items = self.get_items(menu)
             self.cur_tab['page'].update(items)
             self.draw()
         elif k in set('hjkl '):
             LOG.info('handle')
             cur_page = self.cur_tab['cur_page']
             cur_page = self.cur_tab[cur_page]
             cur_page.handle(k)
             cur_page.draw()
Esempio n. 8
0
    def search(self,
               ctx,
               limit=DEFAULT_LIMIT,
               offset=DEFAULT_OFFSET,
               account_type_id=None,
               terms=None) -> (List[AccountType], int):
        """
        search account_type in the database.

        A une utilité ??

        :raise IntMustBePositiveException
        """

        if limit < 0:
            raise IntMustBePositive('limit')

        if offset < 0:
            raise IntMustBePositive('offset')

        result, count = self.account_type_repository.search_account_type_by(
            ctx, account_type_id=account_type_id, terms=terms)

        # Log action.
        LOG.info('account_type_search',
                 extra=log_extra(ctx,
                                 account_type_id=account_type_id,
                                 terms=terms))
        return result, count
Esempio n. 9
0
    def search_room_by(self, ctx, limit=DEFAULT_LIMIT, offset=DEFAULT_OFFSET, room_number=None, owner_username=None,
                       terms=None) -> (
            List[Room], int):
        LOG.debug("sql_room_repository_search_room_by_called",
                  extra=log_extra(ctx, username=owner_username, terms=terms))
        s = ctx.get(CTX_SQL_SESSION)
        q = s.query(Chambre)

        if room_number:
            q = q.filter(Chambre.numero == room_number)

        if terms:
            q = q.filter(or_(
                Chambre.telephone.contains(terms),
                Chambre.description.contains(terms),
            ))

        if owner_username:
            q = q.join(Adherent)
            q = q.filter(Adherent.login == owner_username)

        count = q.count()
        q = q.order_by(Chambre.numero.asc())
        q = q.offset(offset)
        q = q.limit(limit)
        r = q.all()
        r = list(map(_map_room_sql_to_entity, r))
        return r, count
Esempio n. 10
0
    def update(self, ctx, port_id, mutation_request: MutationRequest) -> None:
        """
        Update a port in the database.
        User story: As an admin, I can update a port in the database, so I can modify its description.

        :raise PortNotFound
        :raise SwitchNotFound
        :raise RoomNotFound
        """
        # Make sure the request is valid.
        mutation_request.validate()

        fields_to_update = asdict(mutation_request)
        fields_to_update = {
            k: v
            for k, v in fields_to_update.items() if v is not None
        }

        self.port_repository.update_port(ctx,
                                         port_id=port_id,
                                         **fields_to_update)
        LOG.info("port_update",
                 extra=log_extra(ctx,
                                 mutation=json.dumps(fields_to_update,
                                                     sort_keys=True)))
Esempio n. 11
0
    def post(self, body):
        s = g.session
        firstname = body.get("firstname")
        lastname = body.get("lastname")
        if not firstname or not lastname:
            return "Empty first or last name", 400

        token = secrets.token_urlsafe(TOKEN_SIZE)
        now = datetime.datetime.now()
        end = datetime.datetime(year=now.year,
                                month=now.month,
                                day=now.day,
                                hour=20,
                                minute=0)
        naina = NainA(
            first_name=firstname,
            last_name=lastname,
            access_token=token,
            start_time=now,
            expiration_time=end,
            admin=g.admin.login,
        )
        s.add(naina)

        LOG.info("%s created a temporary account for '%s %s'", g.admin.login,
                 firstname, lastname)

        return {"accessToken": "NAINA_{}".format(token)}, 200
Esempio n. 12
0
    def create(self, ctx, mutation_request: MutationRequest) -> str:
        """
        Create a port in the database.
        User story: As an admin, I can create a port in the database, so I can add a new port to a room.

        :return: the newly created port ID

        :raise ReadOnlyField
        :raise SwitchNotFound
        :raise RoomNotFound
        """
        # Make sure the request is valid.
        mutation_request.validate()

        fields_to_update = asdict(mutation_request)
        fields_to_update = {
            k: v
            for k, v in fields_to_update.items() if v is not None
        }
        port_id = self.port_repository.create_port(ctx, **fields_to_update)
        LOG.info("port_create",
                 extra=log_extra(ctx,
                                 mutation=json.dumps(fields_to_update,
                                                     sort_keys=True)))
        return port_id
Esempio n. 13
0
    def search_account_by(self,
                          ctx,
                          limit=None,
                          offset=None,
                          account_id=None,
                          terms=None) -> (List[Account], int):
        """
        Search for an account.
        """
        LOG.debug("sql_account_repository_search_called",
                  extra=log_extra(ctx, account_id=account_id, terms=terms))
        s = ctx.get(CTX_SQL_SESSION)

        q = s.query(SQLAccount)

        if account_id:
            q = q.filter(SQLAccount.id == account_id)
        if terms:
            q = q.filter(SQLAccount.name.contains(terms))

        count = q.count()
        q = q.order_by(SQLAccount.creation_date.asc())
        q = q.offset(offset)
        q = q.limit(limit)
        r = q.all()

        return list(map(_map_account_sql_to_entity, r)), count
Esempio n. 14
0
    def create_account(self,
                       ctx,
                       name=None,
                       actif=None,
                       type=None,
                       creation_date=None):
        """
        Create an account.

        :raise AccountTypeNotFound ?
        """

        s = ctx.get(CTX_SQL_SESSION)
        LOG.debug("sql_account_repository_create_account_called",
                  extra=log_extra(ctx, name=name, type=type))

        now = datetime.now()

        account = SQLAccount(
            name=name,
            actif=actif,
            type=type,
            creation_date=now,
        )

        with track_modifications(ctx, s, account):
            s.add(account)

        return account
Esempio n. 15
0
    def create_device(self,
                      ctx,
                      mac_address=None,
                      owner_username=None,
                      connection_type=None,
                      ip_v4_address=None,
                      ip_v6_address=None):
        LOG.debug("sql_device_repository_create_device_called",
                  extra=log_extra(ctx, mac=mac_address))
        s = ctx.get(CTX_SQL_SESSION)

        all_devices = get_all_devices(s)
        device = s.query(all_devices).filter(
            all_devices.columns.mac == mac_address).one_or_none()

        if device is not None:
            raise DeviceAlreadyExist()

        # If the user do not change the connection type, we just need to update...
        if connection_type == DeviceType.Wired:
            create_wired_device(
                ctx,
                s=s,
                mac_address=mac_address,
                ip_v4_address=ip_v4_address or 'En Attente',
                ip_v6_address=ip_v6_address or 'En Attente',
                username=owner_username,
            )
        else:
            create_wireless_device(
                ctx,
                s=s,
                mac_address=mac_address,
                username=owner_username,
            )
Esempio n. 16
0
    def create_room(self, ctx, room_number=None, description=None, phone_number=None, vlan_number=None) -> None:
        LOG.debug("sql_room_repository_create_room_called",
                  extra=log_extra(ctx, room_number=room_number, description=description, phone_number=phone_number,
                                  vlan_number=vlan_number))
        s = ctx.get(CTX_SQL_SESSION)
        now = datetime.now()

        result = s.query(Chambre).filter(Chambre.numero == room_number).one_or_none()
        if result is not None:
            raise RoomAlreadyExists()

        vlan = s.query(Vlan).filter(Vlan.numero == vlan_number).one_or_none()
        if vlan is None:
            raise VLANNotFoundError(vlan_number)

        room = Chambre(
            numero=int(room_number),
            description=description,
            telephone=phone_number,
            created_at=now,
            updated_at=now,
            vlan=vlan,
        )

        s.add(room)
Esempio n. 17
0
    def health(self, ctx):
        LOG.debug("http_health_called", extra=log_extra(ctx))

        if self.health_manager.is_healthy(ctx):
            return "OK", 200
        else:
            return "FAILURE", 503
Esempio n. 18
0
    def search(self,
               ctx,
               limit=DEFAULT_LIMIT,
               offset=DEFAULT_OFFSET,
               account_id=None,
               terms=None) -> (List[Transaction], int):
        """
        search transactions in the database.

        :raise IntMustBePositiveException
        """
        if limit < 0:
            raise IntMustBePositive('limit')

        if offset < 0:
            raise IntMustBePositive('offset')

        result, count = self.transaction_repository.search_transaction_by(
            ctx,
            limit=limit,
            offset=offset,
            account_id=account_id,
            terms=terms)

        # Log action.
        LOG.info('transaction_search',
                 extra=log_extra(
                     ctx,
                     account_id=account_id,
                     terms=terms,
                 ))
        return result, count
Esempio n. 19
0
    def search(self,
               ctx,
               limit=DEFAULT_LIMIT,
               offset=DEFAULT_OFFSET,
               terms=None) -> (List[Room], int):
        """
        Search a room in the database.

        User story: As an admin, I can search rooms, so that see the room information.

        :raise IntMustBePositiveException
        """
        if limit < 0:
            raise IntMustBePositive('limit')

        if offset < 0:
            raise IntMustBePositive('offset')

        result, count = self.room_repository.search_room_by(ctx,
                                                            limit=limit,
                                                            offset=offset,
                                                            terms=terms)
        LOG.info('room_search', extra=log_extra(
            ctx,
            terms=terms,
        ))
        return result, count
Esempio n. 20
0
    def search(self,
               ctx,
               limit=DEFAULT_LIMIT,
               offset=DEFAULT_OFFSET,
               username=None,
               terms=None):
        """ Filter the list of the devices according to some criterias """
        LOG.debug("http_device_search_called",
                  extra=log_extra(ctx,
                                  limit=limit,
                                  offset=offset,
                                  username=username,
                                  terms=terms))

        try:
            result, count = self.device_manager.search(ctx, limit, offset,
                                                       username, terms)

        except UserInputError as e:
            return bad_request(e), 400

        headers = {
            "X-Total-Count": count,
            "access-control-expose-headers": "X-Total-Count"
        }
        return list(map(_map_device_to_http_response, result)), 200, headers
Esempio n. 21
0
    def _init_chord_tab(self):
        menus = [
            MenuItem('根  音', ALL_NOTES),
            MenuItem('类  型', list(CHORD_TYPES.keys())),
            MenuItem('开  闭', ['全部', '开放和弦', '封闭和弦']),
            MenuItem('转  位', ['原位', '全部']),
            MenuItem('调  弦', list(TUNINGS.keys())),
            MenuItem('音  色', list(INSTRUMENTS.keys())),
            MenuItem('符  号', ['随机'] + CHORD_SYMBOLS),
        ]
        menu = MenuPage(self.hei - 3,
                        self.wid // sum(self.ration) * self.ration[0],
                        0,
                        0,
                        items=menus,
                        chosen=True)
        LOG.info(f'the menu of chord tab created. winlist:{menu.winlist}')

        chords = self.get_chord(*[item.cur for item in menus])
        page = ChordPage(self.hei - 3,
                         self.wid // sum(self.ration) * self.ration[1],
                         0,
                         self.wid // sum(self.ration) * self.ration[0],
                         items=chords,
                         chosen=False)
        LOG.info(f'the page of chord tab created. winlist:{page.winlist}')
        return {
            'name': 'chord',
            'menu': menu,
            'page': page,
            'cur_page': 'menu'
        }
Esempio n. 22
0
    def put(self, ctx, mac_address, body):
        """ Put (update or create) a new device in the database """
        LOG.debug("http_device_put_called",
                  extra=log_extra(ctx, mac=mac_address, request=body))

        try:
            created = self.device_manager.update_or_create(
                ctx,
                mac_address=mac_address,
                req=MutationRequest(
                    owner_username=body.get('username'),
                    mac_address=body.get('mac'),
                    connection_type=body.get('connection_type'),
                    ip_v4_address=body.get('ip_address'),
                    ip_v6_address=body.get('ipv6_address'),
                ),
            )

            if created:
                return NoContent, 201  # 201 Created
            else:
                return NoContent, 204  # 204 No Content

        except NoMoreIPAvailableException:
            return "IP allocation failed.", 503

        except UserInputError as e:
            return bad_request(e), 400
    def search_switches_by(self,
                           ctx,
                           limit=DEFAULT_LIMIT,
                           offset=DEFAULT_OFFSET,
                           switch_id: str = None,
                           terms: str = None) -> (List[Switch], int):
        """
        Search for a switch.
        """
        LOG.debug("sql_network_object_repository_search_switch_by",
                  extra=log_extra(ctx, switch_id=switch_id, terms=terms))

        s = ctx.get(CTX_SQL_SESSION)

        q = s.query(SwitchSQL)

        if switch_id is not None:
            q = q.filter(SwitchSQL.id == switch_id)

        if terms is not None:
            q = q.filter(
                or_(
                    SwitchSQL.description.contains(terms),
                    SwitchSQL.ip.contains(terms),
                ))

        count = q.count()
        q = q.order_by(SwitchSQL.description.asc())
        q = q.offset(offset)
        q = q.limit(limit)
        result = q.all()

        result = map(_map_switch_sql_to_entity, result)
        result = list(result)
        return result, count
Esempio n. 24
0
    def create(self, ctx, mutation_request: MutationRequest) -> str:
        """
        Create a switch in the database.
        User story: As an admin, I can create a switch in the database, so I can add a new building.

        :return: the newly created switch ID

        :raise ReadOnlyField
        """
        # Make sure the request is valid.
        mutation_request.validate()

        fields_to_update = asdict(mutation_request)
        fields_to_update = {
            k: v
            for k, v in fields_to_update.items() if v is not None
        }

        switch_id = self.switch_repository.create_switch(
            ctx, **fields_to_update)
        LOG.info("switch_create",
                 extra=log_extra(ctx,
                                 mutation=json.dumps(fields_to_update,
                                                     sort_keys=True)))

        return switch_id
Esempio n. 25
0
    def search(self,
               ctx,
               limit=DEFAULT_LIMIT,
               offset=DEFAULT_OFFSET,
               product_id=None,
               terms=None) -> (List[Product], int):
        """
        search product in the database.

        user story: as an admin, i want to have a list of products with some filters, so that i can browse and find
        products.

        :raise intmustbepositiveexception
        """
        if limit < 0:
            raise IntMustBePositive('limit')

        if offset < 0:
            raise IntMustBePositive('offset')

        result, count = self.product_repository.search_product_by(
            ctx,
            limit=limit,
            offset=offset,
            product_id=product_id,
            terms=terms)

        # Log action.
        LOG.info('product_search',
                 extra=log_extra(
                     ctx,
                     product_id=product_id,
                     terms=terms,
                 ))
        return result, count
Esempio n. 26
0
    def update(self, ctx, switch_id: str,
               mutation_request: MutationRequest) -> None:
        """
        Update a switch in the database.
        User story: As an admin, I can update a switch in the database, so I update its community string.

        :raise SwitchNotFound
        """
        # Make sure the request is valid.
        mutation_request.validate()

        fields_to_update = asdict(mutation_request)
        fields_to_update = {
            k: v
            for k, v in fields_to_update.items() if v is not None
        }
        try:
            self.switch_repository.update_switch(ctx,
                                                 switch_id=switch_id,
                                                 **fields_to_update)
            LOG.info("switch_update",
                     extra=log_extra(ctx,
                                     mutation=json.dumps(fields_to_update,
                                                         sort_keys=True)))

        except SwitchNotFoundError as e:
            raise SwitchNotFoundError(switch_id) from e
Esempio n. 27
0
    def search(self,
               ctx,
               limit=DEFAULT_LIMIT,
               offset=DEFAULT_OFFSET,
               room_number=None,
               terms=None) -> (List[Member], int):
        """
        search member in the database.

        user story: as an admin, i want to have a list of members with some filters, so that i can browse and find
        members.

        :raise intmustbepositiveexception
        """
        if limit < 0:
            raise IntMustBePositive('limit')

        if offset < 0:
            raise IntMustBePositive('offset')

        result, count = self.member_repository.search_member_by(
            ctx,
            limit=limit,
            offset=offset,
            room_number=room_number,
            terms=terms)

        # Log action.
        LOG.info('member_search',
                 extra=log_extra(
                     ctx,
                     room_number=room_number,
                     terms=terms,
                 ))
        return result, count
Esempio n. 28
0
    def search_device_by(self, ctx, limit=DEFAULT_LIMIT, offset=DEFAULT_OFFSET, mac_address=None, username=None,
                         terms=None) \
            -> (List[Device], int):
        LOG.debug("sql_device_repository_search_called", extra=log_extra(ctx))
        s = ctx.get(CTX_SQL_SESSION)

        # Return a subquery with all devices (wired & wireless)
        # The fields, ip, ipv6, dns, etc, are set to None for wireless devices
        # There is also a field "type" wich is wired and wireless
        all_devices = get_all_devices(s)

        # Query all devices and their owner's unsername
        q = s.query(all_devices, Adherent.login.label("login"))
        q = q.join(Adherent, Adherent.id == all_devices.columns.adherent_id)

        if mac_address:
            q = q.filter(all_devices.columns.mac == mac_address)

        if username:
            q = q.filter(Adherent.login == username)

        if terms:
            q = q.filter((all_devices.columns.mac.contains(terms))
                         | (all_devices.columns.ip.contains(terms))
                         | (all_devices.columns.ipv6.contains(terms))
                         | (Adherent.login.contains(terms)))
        count = q.count()
        q = q.order_by(all_devices.columns.mac.asc())
        q = q.offset(offset)
        q = q.limit(limit)
        r = q.all()
        results = list(map(_map_device_sql_to_entity, r))

        return results, count
Esempio n. 29
0
    def change_password(self, ctx, username, password) -> None:
        """
        User story: As an admin, I can set the password of a user, so that they can connect using their credentials.
        Change the password of a member.

        BE CAREFUL: do not log the password or store it unhashed.

        :raise PasswordTooShortError
        :raise MemberNotFound
        """

        if len(password) <= 6:  # It's a bit low but eh...
            raise PasswordTooShortError()

        # Overwrite password variable by its hash, now that the checks are done, we don't need the cleartext anymore.
        # Still, be careful not to log this field!
        password = ntlm_hash(password)

        self.member_repository.update_member(ctx, username, password=password)

        LOG.info('member_password_update',
                 extra=log_extra(
                     ctx,
                     username=username,
                 ))
Esempio n. 30
0
    def create_product(self,
                       ctx,
                       name=None,
                       selling_price=None,
                       buying_price=None):
        """
        Create a product .

        :raise ProductTypeNotFound ?
        """

        s = ctx.get(CTX_SQL_SESSION)
        LOG.debug("sql_product_repository_create_product_called",
                  extra=log_extra(ctx, name=name))

        product = SQLProduct(
            name=name,
            buying_price=buying_price,
            selling_price=selling_price,
        )

        with track_modifications(ctx, s, product):
            s.add(product)

        return product